diff --git a/components/ambient-sdk/.gitignore b/components/ambient-sdk/.gitignore new file mode 100644 index 000000000..10d657d78 --- /dev/null +++ b/components/ambient-sdk/.gitignore @@ -0,0 +1,5 @@ +ts-sdk/dist/ +ts-sdk/node_modules/ +python-sdk/*.egg-info/ +python-sdk/venv/ +.claude/settings.local.json diff --git a/components/ambient-sdk/CLAUDE.md b/components/ambient-sdk/CLAUDE.md new file mode 100644 index 000000000..3d6036f0f --- /dev/null +++ b/components/ambient-sdk/CLAUDE.md @@ -0,0 +1,125 @@ +# CLAUDE.md — Ambient Platform SDK + +## Project Intent + +The Ambient Platform SDK provides language-idiomatic HTTP client libraries for the Ambient Code Platform's public REST API. It exists so that external developers and internal automation can create and manage AI agentic sessions **without Kubernetes dependencies or cluster access**. The SDK is the public-facing contract for the platform — everything behind it (CRDs, operators, runners) is an implementation detail. + +## Role in the Platform + +This SDK is one piece of a multi-component system coordinated via `../working.md`: + +| Component | Purpose | Relationship to SDK | +|---|---|---| +| **ambient-api-server** | REST API gateway (Go + Gin) | The server this SDK talks to — implements `/v1/sessions` | +| **ambient-control-plane** | Reconciler / controller | Watches API server for session changes; SDK users never interact with it | +| **ambient-sdk** (this) | Client libraries (Go, Python) | Consumes the API server's public endpoints | +| **Frontend** | NextJS web UI | Will eventually share generated types from `openapi.yaml` | +| **Operator** | Kubernetes controller | Internal only — spawns Jobs from CRs | +| **Runner** | Claude Code CLI executor | Internal only — runs inside Job pods | + +## Quick Reference + +```bash +# Go SDK +cd go-sdk && go test ./... +cd go-sdk/examples && go run main.go + +# Python SDK +cd python-sdk && ./test.sh +cd python-sdk && pip install -e ".[dev]" && pytest +cd python-sdk && python examples/main.py +``` + +### Environment Variables (all SDKs) + +| Variable | Required | Description | +|---|---|---| +| `AMBIENT_TOKEN` | Yes | Bearer token (OpenShift `sha256~`, JWT, or GitHub `ghp_`) | +| `AMBIENT_PROJECT` | Yes | Target project / Kubernetes namespace | +| `AMBIENT_API_URL` | No | API base URL (default: `http://localhost:8080`) | + +## Directory Structure + +``` +ambient-sdk/ +├── CLAUDE.md # This file +├── README.md # Public-facing overview and roadmap +├── docs/ # Detailed documentation +│ ├── architecture.md # Design decisions, platform integration +│ └── authentication.md # Auth flows, token formats, RBAC requirements +├── go-sdk/ # Go client library +│ ├── client/client.go # HTTP client with structured logging and token sanitization +│ ├── types/types.go # Request/response types, SecureToken, input validation +│ ├── examples/main.go # Complete session lifecycle example +│ ├── go.mod # Module: github.com/ambient-code/platform/components/ambient-sdk/go-sdk +│ └── README.md # Go-specific usage and API reference +└── python-sdk/ # Python client library + ├── ambient_platform/ # Package root + │ ├── __init__.py # Public exports, version + │ ├── client.py # AmbientClient with httpx, env-based factory + │ ├── types.py # Dataclasses matching OpenAPI schemas + │ └── exceptions.py # Typed exception hierarchy + ├── examples/main.py # Complete session lifecycle example + ├── test.sh # Integration test runner with env validation + ├── pyproject.toml # Package config (black, isort, mypy, pytest) + └── README.md # Python-specific usage and API reference +``` + +## Code Conventions + +### Go SDK + +- **Go 1.21+**, standard library only (no third-party deps) +- `go fmt ./...` and `golangci-lint run` enforced +- Token stored as plain string with URL sanitization via `sanitizeLogURL()` +- All client constructors return `(*Client, error)` — token validation is mandatory +- Input validation in `NewClient()` for token length and placeholder detection + +### Python SDK + +- **Python 3.8+**, single dependency: `httpx>=0.25.0` +- `black` formatting, `isort` with black profile, `mypy` strict mode +- Dataclasses for all types (no Pydantic — intentionally lightweight) +- `AmbientClient.from_env()` factory for environment-based configuration +- Context manager support (`with AmbientClient(...) as client:`) +- Typed exception hierarchy rooted at `AmbientAPIError` + +### Both SDKs + +- Never log tokens — use `len(token)` or `SecureToken.LogValue()` / `[REDACTED]` +- All request types have `Validate()` / `validate()` methods called before HTTP calls +- API errors return structured `ErrorResponse` without leaking raw response bodies +- Token format validation: OpenShift `sha256~`, JWT (3 dot-separated base64 parts), GitHub `ghp_/gho_/ghu_/ghs_` + +## OpenAPI Specification + +The API server owns the canonical OpenAPI spec at `../ambient-api-server/openapi/openapi.yaml`. The SDK does **not** maintain its own copy — it derives types and client behavior from the API server's spec. + +- **Spec location**: `../ambient-api-server/openapi/` (split by resource: sessions, agents, tasks, workflows, etc.) +- **Session endpoints**: `GET /api/ambient-api-server/v1/sessions`, `POST ...`, `GET .../sessions/{id}` +- **Auth**: `Authorization: Bearer ` header (project scoping via `X-Ambient-Project`) +- **Statuses**: `pending` → `running` → `completed` | `failed` +- Update the API server's spec before changing SDK types or client behavior + +## Security Considerations + +- Tokens are validated on client construction (format, length, placeholder detection) +- Go SDK uses `slog.LogValuer` + `ReplaceAttr` for dual-layer log redaction +- Bearer tokens, SHA256 tokens, and JWTs are pattern-matched and redacted in logs +- API error responses are sanitized before returning to callers +- URL validation rejects placeholder domains (`example.com`) and dangerous schemes + +## Smoke Test + +Run `cd go-sdk && go run examples/main.go` until it passes. This is the SDK's end-to-end smoke test against the live API server. It currently returns 404 because the API server has not been migrated to serve `/api/ambient-api-server/v1/sessions` yet. Once the full migration (api-server + control-plane + deployment) is complete, this test will pass. Keep running it — when it stops returning 404, the platform is wired up. + +## Loadable Context + +| Topic | File | +|---|---| +| Architecture and platform integration | `docs/architecture.md` | +| Authentication, tokens, and RBAC | `docs/authentication.md` | +| Go SDK details | `go-sdk/README.md` | +| Python SDK details | `python-sdk/README.md` | +| API contract (source of truth) | `../ambient-api-server/openapi/openapi.yaml` | +| Cross-session coordination | `../working.md` | diff --git a/components/ambient-sdk/Makefile b/components/ambient-sdk/Makefile new file mode 100644 index 000000000..c90228485 --- /dev/null +++ b/components/ambient-sdk/Makefile @@ -0,0 +1,27 @@ +SPEC_PATH ?= ../ambient-api-server/openapi/openapi.yaml +GO_OUT = go-sdk +PYTHON_OUT = python-sdk/ambient_platform +TS_OUT = ts-sdk +GENERATOR = generator + +.PHONY: generate-sdk verify-sdk build-generator clean + +build-generator: + cd $(GENERATOR) && go build -o ../bin/ambient-sdk-generator . + +generate-sdk: build-generator + ./bin/ambient-sdk-generator \ + -spec $(SPEC_PATH) \ + -go-out $(GO_OUT) \ + -python-out $(PYTHON_OUT) \ + -ts-out $(TS_OUT) + cd $(GO_OUT) && go fmt ./... + +verify-sdk: generate-sdk + cd $(GO_OUT) && go build ./... + cd python-sdk && python3 -c "from ambient_platform import *" + cd $(TS_OUT) && npm run build + @echo "SDK verification passed" + +clean: + rm -f bin/ambient-sdk-generator diff --git a/components/ambient-sdk/README.md b/components/ambient-sdk/README.md new file mode 100644 index 000000000..f6dce5e9f --- /dev/null +++ b/components/ambient-sdk/README.md @@ -0,0 +1,99 @@ +# Ambient Platform SDK + +**Language-idiomatic HTTP client libraries for the Ambient Code Platform's public REST API.** + +## Overview + +The Ambient Platform SDK provides Go, Python, and TypeScript client libraries for interacting with the Ambient Platform API. It exists so that external developers and internal automation can create and manage AI agentic sessions **without Kubernetes dependencies or cluster access**. The SDK is the public-facing contract for the platform. + +## Supported Languages + +- **Go SDK** - `go-sdk/` - Go 1.21+ with standard library only +- **Python SDK** - `python-sdk/` - Python 3.8+ with minimal dependencies +- **TypeScript SDK** - `ts-sdk/` - Modern TypeScript with proper type safety + +## Quick Start + +### Go + +```bash +# Add to go.mod (local development) +require github.com/ambient-code/platform/components/ambient-sdk/go-sdk v0.0.0 + +# Usage +import "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/client" +``` + +### Python + +```bash +pip install -e python-sdk/ +``` + +```python +from ambient_platform.client import AmbientClient + +client = AmbientClient.from_env() +session = client.sessions.create({ + "name": "My Analysis Session", + "prompt": "Analyze this codebase" +}) +``` + +### TypeScript + +```bash +cd ts-sdk && npm install +``` + +```typescript +import { AmbientClient } from './src/client' + +const client = new AmbientClient({ + baseURL: process.env.AMBIENT_API_URL, + token: process.env.AMBIENT_TOKEN, + project: process.env.AMBIENT_PROJECT +}) +``` + +## Environment Variables + +All SDKs support these environment variables: + +| Variable | Required | Description | +|---|---|---| +| `AMBIENT_TOKEN` | Yes | Bearer token (OpenShift `sha256~`, JWT, or GitHub `ghp_`) | +| `AMBIENT_PROJECT` | Yes | Target project / Kubernetes namespace | +| `AMBIENT_API_URL` | No | API base URL (default: `http://localhost:8080`) | + +## API Resources + +The SDK provides access to 4 core resources: + +- **Sessions** - Create and manage AI agentic sessions +- **Users** - User management and authentication +- **Projects** - Project configuration and settings +- **ProjectSettings** - Project-specific configuration + +## Development + +```bash +# Generate all SDKs from OpenAPI spec +make generate-sdk + +# Verify all SDKs build correctly +make verify-sdk + +# Build generator binary +make build-generator +``` + +## Architecture + +The SDK is generated from the OpenAPI specification at `../ambient-api-server/openapi/openapi.yaml` using a custom Go-based generator. This ensures type safety and consistency across all supported languages. + +For detailed documentation, see: +- `docs/architecture.md` - Design decisions and platform integration +- `docs/authentication.md` - Auth flows and token formats +- `go-sdk/README.md` - Go-specific usage +- `python-sdk/README.md` - Python-specific usage \ No newline at end of file diff --git a/components/ambient-sdk/docs/architecture.md b/components/ambient-sdk/docs/architecture.md new file mode 100644 index 000000000..23e53d48b --- /dev/null +++ b/components/ambient-sdk/docs/architecture.md @@ -0,0 +1,122 @@ +# SDK Architecture + +## Design Philosophy + +The Ambient Platform SDK is an **HTTP-first, zero-Kubernetes** client library. It deliberately hides the platform's Kubernetes internals behind simple REST semantics so that consumers never need `kubectl`, `client-go`, or cluster credentials. + +### Core Principles + +1. **Pure HTTP** — Standard REST calls over HTTPS. No CRD watchers, no informers, no leader election. +2. **Minimal Dependencies** — Go SDK uses only the standard library. Python SDK uses only `httpx`. +3. **Type Safety** — Strongly-typed request/response structures in both languages with compile-time (Go) and runtime (Python) validation. +4. **Secure by Default** — Token validation on construction, automatic log redaction, sanitized error surfaces. +5. **API-First** — The API server's `openapi.yaml` is the single source of truth. SDK types derive from it. + +## Platform Integration + +``` + ┌──────────────┐ + │ Frontend │ + │ (NextJS) │ + └──────┬───────┘ + │ +┌──────────────┐ ┌──────▼───────┐ ┌───────────────┐ ┌────────────┐ +│ ambient-sdk │───►│ API Server │───►│ Control Plane │───►│ Operator │ +│ (Go/Python) │ │ (Go + Gin) │ │ (Reconciler) │ │ (K8s Ctrl) │ +└──────────────┘ └──────────────┘ └───────────────┘ └─────┬──────┘ + │ + ┌─────▼──────┐ + │ Runner │ + │ (Claude CLI)│ + └────────────┘ +``` + +### Data Flow + +1. **SDK** sends `POST /v1/sessions` with task, model, and repos to the API server. +2. **API Server** creates an `AgenticSession` Custom Resource in the target namespace. +3. **Control Plane** detects the new CR via polling (`GET /api/ambient-api-server/v1/sessions`). +4. **Operator** watches CRs and spawns a Kubernetes Job. +5. **Runner** pod executes Claude Code CLI, writes results back to the CR status. +6. **SDK** polls `GET /v1/sessions/{id}` until status is `completed` or `failed`. + +### Contract Boundary + +The SDK's contract is defined entirely by the API server's OpenAPI spec (`../ambient-api-server/openapi/`). Everything below the API server is opaque: + +| Visible to SDK | Hidden from SDK | +|---|---| +| `/v1/sessions` endpoints | AgenticSession CRD schema | +| Bearer token + project header | Kubernetes RBAC policies | +| Session status lifecycle | Job scheduling, pod creation | +| JSON request/response shapes | CR spec/status fields | + +## SDK Structure + +### Go SDK (`go-sdk/`) + +``` +client/client.go — HTTP client, request execution, log sanitization +types/types.go — API types, SecureToken, input validators +examples/main.go — Working lifecycle demo +``` + +The Go client is a single struct wrapping `*http.Client` with: +- `SecureToken` for type-safe, log-safe token handling +- `slog`-based structured logging with `ReplaceAttr` sanitizer +- Context-aware methods (`CreateSession`, `GetSession`, `ListSessions`, `WaitForCompletion`) + +### Python SDK (`python-sdk/`) + +``` +ambient_platform/client.py — AmbientClient with httpx +ambient_platform/types.py — Dataclasses matching OpenAPI +ambient_platform/exceptions.py — Typed exception hierarchy +examples/main.py — Working lifecycle demo +``` + +The Python client is a class wrapping `httpx.Client` with: +- Input validation on construction (`_validate_token`, `_validate_project`, `_validate_base_url`) +- `from_env()` factory for environment-based setup +- Context manager support for automatic resource cleanup +- Structured exceptions: `AmbientAPIError` → `AuthenticationError`, `SessionNotFoundError`, `AmbientConnectionError` + +## Session Lifecycle + +``` + POST /v1/sessions + │ + ▼ + ┌─────────┐ ┌─────────┐ ┌───────────┐ + │ pending │────►│ running │────►│ completed │ + └─────────┘ └────┬────┘ └───────────┘ + │ + ▼ + ┌─────────┐ + │ failed │ + └─────────┘ +``` + +- **pending**: CR created, waiting for operator to schedule a Job +- **running**: Job pod is executing the Claude Code CLI +- **completed**: Task finished successfully; `result` field populated +- **failed**: Task failed; `error` field populated + +## Cross-Component Coordination + +The file `../working.md` serves as a coordination protocol between Claude sessions working on different components. Key rules: + +- Read before writing +- Append, don't overwrite +- Tag entries with `[API]` or `[CP]` +- Contracts section defines the agreed API surface + +The SDK depends on the contracts in `working.md` — particularly the session list endpoint and authentication scheme. + +## Future Roadmap + +| Phase | Status | Description | +|---|---|---| +| Phase 1: HTTP-Only Go + Python | Done | Core session CRUD with polling | +| Phase 2: TypeScript SDK | Planned | Generated types from OpenAPI, React Query integration | +| Phase 3: Advanced Features | Planned | OpenTelemetry instrumentation, SDK-based testing utilities | diff --git a/components/ambient-sdk/docs/authentication.md b/components/ambient-sdk/docs/authentication.md new file mode 100644 index 000000000..3dd4c3669 --- /dev/null +++ b/components/ambient-sdk/docs/authentication.md @@ -0,0 +1,165 @@ +# Authentication and Authorization + +## Overview + +The Ambient Platform SDK authenticates via **Bearer token** and scopes requests to a **project** (Kubernetes namespace). Both are required for every API call. + +### Headers + +| Header | Required | Purpose | +|---|---|---| +| `Authorization: Bearer ` | Yes | Authenticates the user | +| `X-Ambient-Project: ` | Yes | Scopes request to a namespace | +| `Content-Type: application/json` | Yes (POST) | Request body format | + +## Token Formats + +The SDK validates tokens on client construction. Accepted formats: + +### OpenShift SHA256 Tokens + +``` +sha256~_3FClshuberfakepO_BGI_tZg_not_real_token_Jv72pRN-r5o +``` + +- Prefix: `sha256~` +- Minimum length: 20 characters +- Obtained via: `oc whoami -t` + +### JWT Tokens + +``` +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature +``` + +- Three dot-separated base64url-encoded parts +- Each part must be non-empty and contain only `[a-zA-Z0-9_-]` +- Minimum total length: 50 characters + +### GitHub Tokens + +``` +ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +- Prefixes: `ghp_`, `gho_`, `ghu_`, `ghs_` +- Minimum length: 40 characters + +### Generic Tokens + +- Minimum length: 20 characters +- Must contain both alphabetic and numeric characters + +## Token Validation + +Both SDKs validate tokens before making any API calls: + +1. **Empty check** — Token cannot be empty +2. **Placeholder detection** — Rejects common placeholders (`YOUR_TOKEN_HERE`, `token`, `password`, `secret`, `example`, `test`, `demo`, `placeholder`, `TODO`) +3. **Minimum length** — At least 10 characters +4. **Format validation** — Checks against known token format rules above + +### Go + +```go +client, err := client.NewClient(baseURL, token, project) +if err != nil { + // Token validation failed + log.Fatal(err) +} +``` + +### Python + +```python +try: + client = AmbientClient(base_url, token, project) +except ValueError as e: + # Token validation failed + print(f"Invalid token: {e}") +``` + +## Log Safety + +Tokens are never written to logs in plaintext. + +### Go — Dual-Layer Redaction + +1. **`SecureToken.LogValue()`** — Implements `slog.LogValuer`. Any `SecureToken` logged via `slog` automatically renders as `sha256***(_chars)` instead of the raw value. + +2. **`sanitizeLogAttrs()`** — A `slog.ReplaceAttr` function applied to the logger that catches sensitive values by: + - Key name: `token`, `password`, `secret`, `apikey`, `authorization` (case variants) + - Key suffix: `_token`, `_password`, `_secret`, `_key` + - Value pattern: `Bearer ` prefix, `sha256~` prefix, JWT structure (`ey...` with 2+ dots) + +### Python + +The Python SDK does not log tokens. The `httpx.Client` is configured with auth headers at construction time, and no debug logging exposes them. + +## Project Validation + +The project name maps to a Kubernetes namespace: + +- Cannot be empty +- Alphanumeric characters, hyphens, and underscores only +- Maximum 63 characters (Kubernetes namespace limit) + +## RBAC Requirements + +The authenticated user must have these Kubernetes RBAC permissions in the target namespace: + +```yaml +- apiGroups: ["vteam.ambient-code"] + resources: ["agenticsessions"] + verbs: ["get", "list", "create"] + +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get"] +``` + +## Environment Variables + +| Variable | Required | Default | Description | +|---|---|---|---| +| `AMBIENT_TOKEN` | Yes | — | Bearer authentication token | +| `AMBIENT_PROJECT` | Yes | — | Target project (Kubernetes namespace) | +| `AMBIENT_API_URL` | No | `http://localhost:8080` | API server base URL | + +### Quick Setup + +```bash +# OpenShift users +export AMBIENT_TOKEN="$(oc whoami -t)" +export AMBIENT_PROJECT="$(oc project -q)" +export AMBIENT_API_URL="https://public-api-route-yournamespace.apps.your-cluster.com" + +# Manual setup +export AMBIENT_TOKEN="your-bearer-token" +export AMBIENT_PROJECT="your-namespace" +export AMBIENT_API_URL="https://api.ambient-code.io" +``` + +## Common Errors + +| HTTP Status | Error | Cause | Fix | +|---|---|---|---| +| 400 | `Project required` | Missing `X-Ambient-Project` header | Set `AMBIENT_PROJECT` env var | +| 401 | `Unauthorized` | Invalid or expired token | Refresh token via `oc login` or regenerate | +| 403 | `Forbidden` | Insufficient RBAC permissions | Request `agenticsessions` access in the namespace | +| 404 | `Session not found` | Wrong session ID or no access to namespace | Verify ID and project match | + +### Diagnosing Permission Issues + +```bash +# Verify identity +oc whoami + +# Check token validity +oc whoami -t + +# Test RBAC permissions +oc auth can-i create agenticsessions.vteam.ambient-code -n +oc auth can-i list agenticsessions.vteam.ambient-code -n +oc auth can-i get agenticsessions.vteam.ambient-code -n +``` diff --git a/components/ambient-sdk/generator.md b/components/ambient-sdk/generator.md new file mode 100644 index 000000000..2c9e4e058 --- /dev/null +++ b/components/ambient-sdk/generator.md @@ -0,0 +1,1206 @@ +# Ambient SDK Generator + +## Overview + +The Ambient SDK generator automatically produces Go, Python, and TypeScript client libraries from the canonical OpenAPI specification. This ensures the SDKs stay in perfect sync with the API server while providing idiomatic, type-safe interfaces in each language. + +## Problem + +The SDK handles multiple resources that follow identical CRUD patterns. Hand-writing clients for each resource across three languages (Go, Python, TypeScript) would produce thousands of lines of near-identical code that drifts from the API server's OpenAPI spec between releases. When the API server adds fields, resources, or endpoints, the SDK must be updated in lockstep — manually doing this is error-prone and slow. + +## Goal + +A code generator that reads the canonical `openapi.yaml` and produces idiomatic Go, Python, and TypeScript SDK code. The generated SDKs use the **Builder pattern** for human-readable request construction while keeping response types simple and transparent. Re-running the generator on any OpenAPI change produces a correct SDK with zero manual intervention. + +## OpenAPI Shape Analysis + +### What the spec tells us + +Every resource in the API follows the same template: + +``` +Resource = ObjectReference + resource-specific fields (allOf) +ResourceList = List + items[]Resource (allOf) +PatchRequest = flat object with resource-specific mutable fields +``` + +Base schemas (shared, never change per-resource): + +| Schema | Fields | +|-----------------|------------------------------------------------| +| ObjectReference | id, kind, href, created_at, updated_at | +| List | kind, page, size, total (+ items in each *List) | +| Error | ObjectReference + code, reason, operation_id | + +Every list endpoint accepts 5 query params: `page`, `size`, `search`, `orderBy`, `fields`. + +Every resource supports: `GET /resources` (list), `POST /resources` (create), `GET /resources/{id}` (get), `PATCH /resources/{id}` (update). User additionally supports `DELETE`. + +### Current 4 resources and their specific fields + +| Resource | Specific Fields (beyond ObjectReference) | Required | Has Delete | +|-----------------|-----------------------------------------------------------------------------|-----------------|------------| +| Session | name, prompt, interactive, timeout, llm_*, repos, phase, start_time, etc. | name, prompt | No | +| Project | name, display_name, description, repos, members | name | Yes | +| ProjectSettings | project_id, settings, updated_by | project_id | Yes | +| User | username, email, display_name | username, email | No | + +The API has been significantly pruned from 8 resources to 4 core resources that represent the essential platform entities. + +## Quick Start + +### Running the Generator + +```bash +# From the generator directory +cd components/ambient-sdk/generator + +# Generate all three SDKs +go run . \ + --spec ../../ambient-api-server/openapi/openapi.yaml \ + --go-out ../go-sdk \ + --python-out ../python-sdk \ + --ts-out ../ts-sdk + +# Or generate just one SDK +go run . --spec ../../ambient-api-server/openapi/openapi.yaml --go-out ../go-sdk +``` + +### Command Line Options + +| Flag | Required | Description | +|------|----------|-------------| +| `--spec` | Yes | Path to the main OpenAPI specification file | +| `--go-out` | No | Output directory for Go SDK (generates if specified) | +| `--python-out` | No | Output directory for Python SDK (generates if specified) | +| `--ts-out` | No | Output directory for TypeScript SDK (generates if specified) | + +At least one output directory must be specified. + +### Generated Output Structure + +``` +ambient-sdk/ +├── go-sdk/ # Go SDK output +│ ├── client/ # API client methods +│ ├── types/ # Resource types and builders +│ └── examples/ # Usage examples +├── python-sdk/ # Python SDK output +│ ├── ambient_platform/ # Main package +│ ├── examples/ # Usage examples +│ └── tests/ # Generated tests +└── ts-sdk/ # TypeScript SDK output + ├── src/ # Source files + ├── examples/ # Usage examples + └── tests/ # Generated tests +``` + +## Architecture + +### What gets generated vs. what stays hand-written + +``` +ambient-sdk/ +├── generator/ # THE GENERATOR (hand-written, runs at build time) +│ ├── main.go # CLI entry point: reads openapi.yaml, emits Go + Python +│ ├── parser.go # OpenAPI YAML parser (resolves $ref, allOf) +│ ├── model.go # Intermediate representation (Resource, Field, Endpoint) +│ ├── templates/ +│ │ ├── go/ +│ │ │ ├── types.go.tmpl # Per-resource type + builder +│ │ │ ├── client.go.tmpl # Per-resource client methods +│ │ │ ├── base.go.tmpl # ObjectReference, List, Error, ListOptions +│ │ │ └── iterator.go.tmpl # Pagination iterator +│ │ ├── python/ +│ │ │ ├── types.py.tmpl # Per-resource dataclass + builder +│ │ │ ├── client.py.tmpl # Per-resource client methods +│ │ │ ├── base.py.tmpl # ObjectReference, List, Error +│ │ │ └── iterator.py.tmpl # Pagination iterator +│ │ └── ts/ +│ │ ├── types.ts.tmpl # Per-resource interface + builder +│ │ ├── client.ts.tmpl # Per-resource client methods +│ │ ├── base.ts.tmpl # ObjectReference, List, Error +│ │ └── index.ts.tmpl # Main exports +│ └── generator_test.go # Golden-file tests +│ +├── go-sdk/ # GENERATED OUTPUT (do not hand-edit) +│ ├── types/ +│ │ ├── base.go # generated: ObjectReference, ListMeta, APIError +│ │ ├── session.go # generated: Session, SessionBuilder, SessionPatchBuilder +│ │ ├── agent.go # generated: Agent, AgentBuilder, ... +│ │ ├── ... (one per resource) +│ │ └── list_options.go # generated: ListOptions builder +│ ├── client/ +│ │ ├── client.go # HAND-WRITTEN: Client struct, auth, SecureToken, sanitizeLogAttrs +│ │ ├── session_api.go # generated: Sessions() resource accessor +│ │ ├── agent_api.go # generated: Agents() resource accessor +│ │ ├── ... (one per resource) +│ │ └── iterator.go # generated: generic pagination iterator +│ ├── examples/main.go # hand-written +│ ├── go.mod +│ └── README.md +│ +├── python-sdk/ # GENERATED OUTPUT (do not hand-edit) +│ ├── ambient_platform/ +│ │ ├── __init__.py # hand-written (public exports, version) +│ │ ├── _base.py # generated: ObjectReference, ListMeta, APIError +│ │ ├── session.py # generated: Session, SessionBuilder, SessionPatch +│ │ ├── project.py # generated: Project, ProjectBuilder, ... +│ │ ├── ... (one per resource) +│ │ ├── client.py # HAND-WRITTEN: AmbientClient, auth, from_env(), context manager +│ │ ├── _session_api.py # generated: SessionAPI mixin +│ │ ├── _project_api.py # generated: ProjectAPI mixin +│ │ ├── ... (one per resource) +│ │ ├── _iterator.py # generated: pagination iterator +│ │ └── exceptions.py # hand-written +│ ├── examples/main.py +│ ├── pyproject.toml +│ └── README.md +│ +└── ts-sdk/ # GENERATED OUTPUT (do not hand-edit) + ├── src/ + │ ├── base.ts # generated: ObjectReference, List, APIError + │ ├── session.ts # generated: Session interface + builder + │ ├── project.ts # generated: Project interface + builder + │ ├── ... (one per resource) + │ ├── client.ts # generated: AmbientClient class + │ ├── session_api.ts # generated: SessionAPI methods + │ ├── project_api.ts # generated: ProjectAPI methods + │ ├── ... (one per resource) + │ └── index.ts # generated: main exports + ├── package.json + ├── tsconfig.json + └── README.md +``` + +### Why a custom generator instead of openapi-generator? + +1. **Builder pattern.** Standard openapi-generator emits flat structs/classes with constructors. We want `NewSessionBuilder().Name("x").Prompt("y").Build()` — that requires custom templates regardless. +2. **Minimal dependencies.** Go SDK stays stdlib-only. Python SDK stays httpx-only. TypeScript SDK has minimal deps. openapi-generator introduces runtime libraries. +3. **The spec is highly uniform.** 4 resources, same CRUD pattern, same pagination, same error schema. A custom generator is ~800 lines of Go; openapi-generator is a 200MB Java runtime generating thousands of lines of framework code we'd immediately delete. +4. **Security.** Hand-written token handling, URL validation, and log sanitization stay outside the generated boundary. We don't want a generator touching auth code. +5. **Type safety.** Full compile-time validation in Go, mypy compliance in Python, and strict TypeScript types. + +## Security Features + +The generator includes several security enhancements: + +### URL Injection Protection +- All ID parameters are URL-escaped using `url.PathEscape()` +- Prevents path traversal attacks through resource IDs +- Applied automatically in all generated client methods + +### Input Validation +- Placeholder URL detection (`example.com`, `placeholder`) +- Token format validation (OpenShift `sha256~`, JWT, GitHub tokens) +- Required field validation in builders + +### Log Security +- Token redaction in logs (`[REDACTED]` replacement) +- URL sanitization to prevent credential leakage +- Safe error message formatting + +### TypeScript Safety +- No unsafe type casting (`as any`) +- Proper type validation with runtime checks +- Explicit error handling for invalid responses + +Example security validation: + +```typescript +// Generated TypeScript with validation +function validateResponse(data: unknown): Session { + if (!isRecord(data)) { + throw new Error('Invalid response format'); + } + return data as Session; // Safe after validation +} +``` + +## Builder Pattern Design + +### Go: Fluent Builder with Compile-Time Safety + +```go +// --- CREATING --- +session, err := client.Sessions().Create(ctx, + ambient.NewSessionBuilder(). + Name("analyze-security-report"). + Prompt("Review the latest CVE report and summarize findings"). + RepoURL("https://github.com/org/repo"). + AssignedUserID("user-123"). + Build(), +) + +// --- LISTING with pagination --- +sessions, err := client.Sessions().List(ctx, + ambient.NewListOptions(). + Page(2). + Size(50). + Search("name like 'security%'"). + OrderBy("created_at desc"). + Build(), +) + +// --- PATCHING --- +updated, err := client.Sessions().Update(ctx, sessionID, + ambient.NewSessionPatchBuilder(). + Name("updated-name"). + Prompt("new prompt"). + Build(), +) + +// --- GET --- +session, err := client.Sessions().Get(ctx, sessionID) + +// --- ITERATING all pages --- +iter := client.Sessions().ListAll(ctx, + ambient.NewListOptions().Size(100).Build(), +) +for iter.Next() { + session := iter.Item() + fmt.Println(session.Name) +} +if err := iter.Err(); err != nil { + log.Fatal(err) +} +``` + +### Python: Fluent Builder with Chainable Methods + +```python +# --- CREATING --- +session = client.sessions.create( + Session.builder() + .name("analyze-security-report") + .prompt("Review the latest CVE report and summarize findings") + .repo_url("https://github.com/org/repo") + .assigned_user_id("user-123") + .build() +) + +# --- LISTING with pagination --- +sessions = client.sessions.list( + ListOptions() + .page(2) + .size(50) + .search("name like 'security%'") + .order_by("created_at desc") +) + +# --- PATCHING --- +updated = client.sessions.update(session_id, + SessionPatch() + .name("updated-name") + .prompt("new prompt") +) + +# --- GET --- +session = client.sessions.get(session_id) + +# --- ITERATING all pages --- +for session in client.sessions.list_all(size=100): + print(session.name) +``` + +## Generated Type Details + +### Go Types (per resource) + +```go +// Session represents an Ambient Platform Session resource. +// This type is GENERATED from openapi.yaml — do not edit. +type Session struct { + // ObjectReference fields (embedded) + ID string `json:"id,omitempty"` + Kind string `json:"kind,omitempty"` + Href string `json:"href,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + + // Session-specific fields + Name string `json:"name"` + RepoURL string `json:"repo_url,omitempty"` + Prompt string `json:"prompt,omitempty"` + CreatedByUserID string `json:"created_by_user_id,omitempty"` + AssignedUserID string `json:"assigned_user_id,omitempty"` + WorkflowID string `json:"workflow_id,omitempty"` +} + +// SessionBuilder constructs a Session for Create requests. +type SessionBuilder struct { + session Session + errors []error +} + +func NewSessionBuilder() *SessionBuilder { + return &SessionBuilder{} +} + +func (b *SessionBuilder) Name(name string) *SessionBuilder { + b.session.Name = name + return b +} + +// ... one method per mutable field ... + +func (b *SessionBuilder) Build() (*Session, error) { + if b.session.Name == "" { + b.errors = append(b.errors, fmt.Errorf("name is required")) + } + if len(b.errors) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...)) + } + return &b.session, nil +} + +// SessionPatchBuilder constructs a SessionPatchRequest. +// Uses *string to distinguish "not set" from "set to empty string". +type SessionPatchBuilder struct { + patch map[string]any +} + +func NewSessionPatchBuilder() *SessionPatchBuilder { + return &SessionPatchBuilder{patch: make(map[string]any)} +} + +func (b *SessionPatchBuilder) Name(name string) *SessionPatchBuilder { + b.patch["name"] = name + return b +} + +func (b *SessionPatchBuilder) Build() map[string]any { + return b.patch +} +``` + +### Python Types (per resource) + +```python +@dataclass(frozen=True) +class Session: + """Ambient Platform Session resource. + GENERATED from openapi.yaml — do not edit. + """ + # ObjectReference fields + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + # Session-specific fields + name: str = "" + repo_url: str = "" + prompt: str = "" + created_by_user_id: str = "" + assigned_user_id: str = "" + workflow_id: str = "" + + @classmethod + def from_dict(cls, data: dict) -> "Session": + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), + name=data.get("name", ""), + repo_url=data.get("repo_url", ""), + prompt=data.get("prompt", ""), + created_by_user_id=data.get("created_by_user_id", ""), + assigned_user_id=data.get("assigned_user_id", ""), + workflow_id=data.get("workflow_id", ""), + ) + + @classmethod + def builder(cls) -> "SessionBuilder": + return SessionBuilder() + + +class SessionBuilder: + """Fluent builder for Session creation requests.""" + + def __init__(self): + self._data: dict[str, Any] = {} + + def name(self, value: str) -> "SessionBuilder": + self._data["name"] = value + return self + + def prompt(self, value: str) -> "SessionBuilder": + self._data["prompt"] = value + return self + + # ... one method per mutable field ... + + def build(self) -> dict: + if "name" not in self._data: + raise ValueError("name is required") + return dict(self._data) + + +class SessionPatch: + """Fluent builder for Session PATCH requests.""" + + def __init__(self): + self._data: dict[str, Any] = {} + + def name(self, value: str) -> "SessionPatch": + self._data["name"] = value + return self + + # ... one method per patchable field ... + + def to_dict(self) -> dict: + return dict(self._data) +``` + +### List Envelope and Pagination + +```go +// ListMeta contains pagination metadata from list responses. +type ListMeta struct { + Kind string `json:"kind"` + Page int `json:"page"` + Size int `json:"size"` + Total int `json:"total"` +} + +// SessionList is a paginated list of Sessions. +type SessionList struct { + ListMeta + Items []Session `json:"items"` +} + +// ListOptions configures list/search/pagination parameters. +type ListOptions struct { + Page int + Size int + Search string + OrderBy string + Fields string +} + +// ListOptionsBuilder constructs ListOptions with fluent API. +type ListOptionsBuilder struct { + opts ListOptions +} + +func NewListOptions() *ListOptionsBuilder { + return &ListOptionsBuilder{opts: ListOptions{Page: 1, Size: 100}} +} + +func (b *ListOptionsBuilder) Page(page int) *ListOptionsBuilder { + b.opts.Page = page + return b +} + +func (b *ListOptionsBuilder) Size(size int) *ListOptionsBuilder { + if size > 65500 { size = 65500 } + b.opts.Size = size + return b +} + +func (b *ListOptionsBuilder) Search(search string) *ListOptionsBuilder { + b.opts.Search = search + return b +} + +func (b *ListOptionsBuilder) OrderBy(orderBy string) *ListOptionsBuilder { + b.opts.OrderBy = orderBy + return b +} + +func (b *ListOptionsBuilder) Fields(fields string) *ListOptionsBuilder { + b.opts.Fields = fields + return b +} + +func (b *ListOptionsBuilder) Build() *ListOptions { + return &b.opts +} +``` + +## Generated Client Methods (per resource) + +### Go Resource API + +```go +// SessionAPI provides CRUD operations for Sessions. +// GENERATED from openapi.yaml — do not edit. +type SessionAPI struct { + client *Client +} + +func (c *Client) Sessions() *SessionAPI { + return &SessionAPI{client: c} +} + +func (a *SessionAPI) Create(ctx context.Context, session *Session) (*Session, error) { + body, err := json.Marshal(session) + if err != nil { + return nil, fmt.Errorf("marshal session: %w", err) + } + var result Session + err = a.client.do(ctx, http.MethodPost, "/sessions", body, http.StatusCreated, &result) + return &result, err +} + +func (a *SessionAPI) Get(ctx context.Context, id string) (*Session, error) { + var result Session + err := a.client.do(ctx, http.MethodGet, "/sessions/"+id, nil, http.StatusOK, &result) + return &result, err +} + +func (a *SessionAPI) List(ctx context.Context, opts *ListOptions) (*SessionList, error) { + var result SessionList + err := a.client.doWithQuery(ctx, http.MethodGet, "/sessions", nil, http.StatusOK, &result, opts) + return &result, err +} + +func (a *SessionAPI) Update(ctx context.Context, id string, patch map[string]any) (*Session, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal patch: %w", err) + } + var result Session + err = a.client.do(ctx, http.MethodPatch, "/sessions/"+id, body, http.StatusOK, &result) + return &result, err +} + +// ListAll returns an iterator that fetches all pages. +func (a *SessionAPI) ListAll(ctx context.Context, opts *ListOptions) *Iterator[Session] { + return NewIterator[Session](func(page int) (*SessionList, error) { + o := *opts + o.Page = page + return a.List(ctx, &o) + }) +} +``` + +### Python Resource API (mixin pattern) + +```python +class SessionAPI: + """GENERATED from openapi.yaml — do not edit.""" + + def create(self, data: dict) -> Session: + resp = self._client._request("POST", "/sessions", json=data) + return Session.from_dict(resp) + + def get(self, session_id: str) -> Session: + resp = self._client._request("GET", f"/sessions/{session_id}") + return Session.from_dict(resp) + + def list(self, opts: Optional[ListOptions] = None) -> SessionList: + resp = self._client._request("GET", "/sessions", params=opts.to_params() if opts else None) + return SessionList.from_dict(resp) + + def update(self, session_id: str, patch) -> Session: + data = patch.to_dict() if hasattr(patch, 'to_dict') else patch + resp = self._client._request("PATCH", f"/sessions/{session_id}", json=data) + return Session.from_dict(resp) + + def list_all(self, **kwargs) -> Iterator[Session]: + """Iterate all pages.""" + page = 1 + size = kwargs.get("size", 100) + while True: + result = self.list(ListOptions().page(page).size(size)) + yield from result.items + if page * size >= result.total: + break + page += 1 +``` + +## Hand-Written Boundary + +The following files are **never generated** and contain security-critical, SDK-specific logic: + +### Go — `client/client.go` (hand-written) + +```go +type Client struct { + baseURL string + basePath string // default: "/api/ambient-api-server/v1" + token SecureToken + project string + httpClient *http.Client + logger *slog.Logger +} + +// do executes an HTTP request. ALL generated *API types call this. +func (c *Client) do(ctx context.Context, method, path string, body []byte, expectedStatus int, result any) error { ... } + +// doWithQuery adds query parameters (ListOptions) to the request. +func (c *Client) doWithQuery(ctx context.Context, method, path string, body []byte, expectedStatus int, result any, opts *ListOptions) error { ... } + +// Functional options for client construction +func WithBasePath(path string) ClientOption { ... } +func WithTimeout(d time.Duration) ClientOption { ... } +func WithHTTPClient(c *http.Client) ClientOption { ... } +``` + +### Python — `client.py` (hand-written) + +```python +class AmbientClient: + def __init__(self, base_url, token, project, *, base_path="/api/ambient-api-server/v1", timeout=30.0): ... + def _request(self, method, path, **kwargs) -> dict: ... # ALL generated APIs call this + + @property + def sessions(self) -> SessionAPI: ... + @property + def agents(self) -> AgentAPI: ... + # ... one property per resource, lazily initialized + + @classmethod + def from_env(cls, **kwargs) -> "AmbientClient": ... +``` + +## Generator Implementation + +### Intermediate Representation + +The generator parses OpenAPI YAML into a language-neutral IR: + +```go +type Resource struct { + Name string // "Session" + Plural string // "sessions" + PathSegment string // "sessions" (from URL path) + Fields []Field // resource-specific fields (not ObjectReference) + RequiredFields []string // fields listed in `required` + HasDelete bool // only User currently +} + +type Field struct { + Name string // "created_by_user_id" + GoName string // "CreatedByUserID" + PythonName string // "created_by_user_id" + Type string // "string", "integer" + Format string // "date-time", "int32", "" + GoType string // "string", "int32", "*time.Time" + PythonType string // "str", "int", "Optional[datetime]" + Required bool + JSONTag string // `json:"created_by_user_id,omitempty"` +} +``` + +### Type Mapping + +| OpenAPI Type + Format | Go Type | Python Type | +|--------------------------|--------------|----------------------| +| `string` | `string` | `str` | +| `string` + `date-time` | `*time.Time` | `Optional[datetime]` | +| `integer` | `int` | `int` | +| `integer` + `int32` | `int32` | `int` | +| `boolean` | `bool` | `bool` | +| `array` of `string` | `[]string` | `list[str]` | + +### Template Execution + +``` +for each resource in parsed_spec: + execute go/types.go.tmpl → go-sdk/types/{resource_snake}.go + execute go/client.go.tmpl → go-sdk/client/{resource_snake}_api.go + execute py/types.py.tmpl → python-sdk/ambient_platform/{resource_snake}.py + execute py/client.py.tmpl → python-sdk/ambient_platform/_{resource_snake}_api.py + +execute go/base.go.tmpl → go-sdk/types/base.go +execute go/iterator.go.tmpl → go-sdk/client/iterator.go +execute py/base.py.tmpl → python-sdk/ambient_platform/_base.py +execute py/iterator.py.tmpl → python-sdk/ambient_platform/_iterator.py +``` + +### Generated File Header + +Every generated file starts with: + +``` +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../ambient-api-server/openapi/openapi.yaml +// Generated: 2026-02-14T17:00:00Z +``` + +This follows Go convention (`go generate` tools) and makes it obvious which files are safe to hand-edit. + +## Build Integration + +### Makefile target + +```makefile +.PHONY: generate-sdk +generate-sdk: + @echo "Generating SDK from OpenAPI spec..." + cd generator && go run . \ + -spec ../ambient-api-server/openapi/openapi.yaml \ + -go-out ../go-sdk \ + -python-out ../python-sdk + cd go-sdk && go fmt ./... + cd python-sdk && black ambient_platform/ && isort ambient_platform/ + @echo "SDK generated successfully. Run tests to verify." + +.PHONY: verify-sdk +verify-sdk: generate-sdk + cd go-sdk && go test ./... + cd python-sdk && pytest +``` + +### CI check (prevent drift) + +```yaml +# In GitHub Actions +- name: Verify SDK is up to date + run: | + make generate-sdk + git diff --exit-code go-sdk/ python-sdk/ + # Fails if generated output differs from committed code +``` + +## Error Handling + +### Generated APIError type + +```go +type APIError struct { + // ObjectReference fields + ID string `json:"id,omitempty"` + Kind string `json:"kind,omitempty"` + Href string `json:"href,omitempty"` + + // Error-specific fields + Code string `json:"code"` + Reason string `json:"reason"` + OperationID string `json:"operation_id,omitempty"` + + // HTTP metadata (not from JSON — set by client) + StatusCode int `json:"-"` +} + +func (e *APIError) Error() string { + return fmt.Sprintf("ambient API error %d: %s — %s", e.StatusCode, e.Code, e.Reason) +} +``` + +```python +@dataclass(frozen=True) +class APIError(Exception): + """Structured API error from the Ambient Platform.""" + status_code: int + code: str + reason: str + operation_id: str = "" + id: str = "" + kind: str = "" + href: str = "" + + def __str__(self) -> str: + return f"ambient API error {self.status_code}: {self.code} — {self.reason}" +``` + +## Pagination Iterator + +### Go (generic, requires Go 1.21+) + +```go +type Listable[T any] interface { + GetItems() []T + GetTotal() int + GetPage() int + GetSize() int +} + +type Iterator[T any] struct { + fetchPage func(page int) (Listable[T], error) + items []T + index int + page int + total int + done bool + err error +} + +func (it *Iterator[T]) Next() bool { + if it.done || it.err != nil { + return false + } + it.index++ + if it.index < len(it.items) { + return true + } + // Fetch next page + it.page++ + result, err := it.fetchPage(it.page) + if err != nil { + it.err = err + return false + } + it.items = result.GetItems() + it.index = 0 + it.total = result.GetTotal() + if len(it.items) == 0 { + it.done = true + return false + } + return true +} + +func (it *Iterator[T]) Item() T { return it.items[it.index] } +func (it *Iterator[T]) Err() error { return it.err } +``` + +### Python (generator-based) + +```python +def paginate(fetch_page, size=100): + """Generic pagination iterator.""" + page = 1 + while True: + result = fetch_page(page=page, size=size) + yield from result.items + if page * size >= result.total: + break + page += 1 +``` + +## Decisions (resolved) + +1. **Generator language: Go.** Consistent with the project. Uses `text/template` (same as K8s code generators), tested with `go test`. + +2. **DELETE scope: follow the OpenAPI spec exactly.** The generator emits Delete methods only when the spec declares a DELETE operation on a resource. Currently only User has DELETE. When other resources gain DELETE in the spec, the generator picks it up automatically. + +3. **Async Python client: yes.** The generator emits both sync (`httpx.Client`) and async (`httpx.AsyncClient`) variants. Async client lives in `_async_client.py` with `AsyncAmbientClient` and async resource API classes. Both share the same generated types and builders. + +4. **Version pinning: yes, SHA256 hash.** The generated file header includes both the timestamp and a SHA256 hash of the concatenated OpenAPI spec files. CI can detect drift by comparing the embedded hash against the current spec hash without re-running the generator. + + ``` + // Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. + // Source: ../ambient-api-server/openapi/openapi.yaml + // Spec SHA256: a1b2c3d4e5f6... + // Generated: 2026-02-14T17:00:00Z + ``` + +5. **Field grouping: keep flat, readOnly self-manages.** When Session expands to ~30 fields, status/runtime fields are `readOnly` in the spec so they appear only on response types, never on builders. Builders stay clean with only mutable fields. No grouping needed. + +## Testing Strategy + +The SDK has three testing tiers: generator tests, SDK unit tests, and end-to-end tests. The e2e tier is the most valuable — the SDK is the natural e2e harness for the entire platform because it exercises the full stack: **SDK → API server → Postgres → control plane → operator → runner pod**. + +Every other component unit-tests itself (`go test ./...` in api-server, cp, operator). The SDK's e2e suite validates that the assembled system works as a whole. + +### Tier 1: Generator Tests (CI, no server) + +Golden-file tests that validate the generator itself produces correct output. + +``` +generator/ +└── generator_test.go +``` + +| Test | What it validates | +|------|-------------------| +| `TestParseOpenAPISpec` | Parser resolves `$ref`, `allOf`, extracts all 4 resources with correct fields | +| `TestGenerateGoTypes` | Generated Go types match golden files (field names, JSON tags, types) | +| `TestGeneratePythonTypes` | Generated Python dataclasses match golden files | +| `TestGenerateGoClient` | Generated API methods match golden files (method signatures, paths) | +| `TestGeneratePythonClient` | Generated Python API methods match golden files | +| `TestBuilderValidation` | Generated builders enforce `required` fields from spec | +| `TestDeleteOnlyWhenDeclared` | Only User gets `Delete()` method; others don't | +| `TestSpecHash` | SHA256 hash in header matches actual spec content | + +**How golden-file tests work:** + +```bash +# Update golden files after intentional changes: +cd generator && go test ./... -update + +# CI runs without -update — any diff = failure: +cd generator && go test ./... +``` + +### Tier 2: SDK Unit Tests (CI, no server) + +Test the generated SDK code in isolation using mocked HTTP responses. + +#### Go SDK + +``` +go-sdk/ +├── types/ +│ ├── base_test.go # ObjectReference, ListMeta, APIError serialization +│ ├── session_test.go # Session JSON round-trip, SessionBuilder, SessionPatchBuilder +│ ├── agent_test.go # Agent JSON round-trip, AgentBuilder +│ ├── ... # One per resource (generated from template) +│ └── list_options_test.go # Query param encoding, size capping at 65500 +├── client/ +│ ├── client_test.go # Auth validation, token format rejection, SecureToken redaction (hand-written) +│ └── iterator_test.go # Multi-page iteration, empty page stop, error propagation +``` + +#### Python SDK + +``` +python-sdk/ +├── tests/ +│ ├── test_types.py # from_dict round-trip for all resources, datetime parsing +│ ├── test_builders.py # Required field enforcement, Build() errors, Patch sparse output +│ ├── test_list_options.py # to_params() encoding, size capping +│ ├── test_client.py # Auth validation, token rejection, from_env() (hand-written) +│ ├── test_iterator.py # Pagination generator, empty page, error propagation +│ └── test_error_parsing.py # JSON error body → APIError with code, reason, operation_id +``` + +#### What unit tests cover per resource (generated from template) + +| Test | Go | Python | +|------|-----|--------| +| JSON round-trip: marshal → unmarshal preserves all fields | `Session{Name:"x"} → json → Session{Name:"x"}` | `Session.from_dict(session.to_dict()) == session` | +| Builder enforces required fields | `NewSessionBuilder().Build()` → error "name is required" | `Session.builder().build()` → `ValueError` | +| Builder sets all fields | `NewSessionBuilder().Name("x").Prompt("y").Build()` → `Session{Name:"x", Prompt:"y"}` | equivalent | +| PatchBuilder emits only set fields | `NewSessionPatchBuilder().Name("x").Build()` → `{"name":"x"}` (no other keys) | equivalent | +| ObjectReference fields present on response | Unmarshal JSON with `id`, `kind`, `href`, `created_at`, `updated_at` | equivalent | +| from_dict ignores unknown fields | Extra JSON keys don't cause errors | equivalent | + +#### Generated vs hand-written test split + +- **Generated** (from templates, one per resource): type round-trip, builder validation, patch sparseness — identical coverage for every resource automatically +- **Hand-written** (security-critical, never generated): `client_test.go` / `test_client.py` — token format validation, placeholder detection, SecureToken log redaction, URL validation, error response sanitization + +### Tier 3: End-to-End Tests (requires live server) + +The SDK e2e suite is the **platform-wide integration test**. It creates real resources via the SDK, verifies they propagate through the full stack, and cleans up. + +``` +ambient-sdk/ +├── e2e/ +│ ├── e2e_test.go # Go e2e suite +│ ├── e2e_test.py # Python e2e suite (identical scenarios) +│ └── README.md # Setup instructions, env vars, CI config +``` + +#### Environment + +```bash +export AMBIENT_API_URL="https://ambient-api.apps.cluster.example.com" +export AMBIENT_TOKEN="sha256~..." +export AMBIENT_PROJECT="e2e-test-project" +``` + +#### E2E Test Scenarios + +| # | Scenario | What it validates across the stack | +|---|----------|-----------------------------------| +| 1 | **Session lifecycle** | Create → Get → List (appears) → Patch name → Get (updated) → verify `updated_at` changed | +| 2 | **Session with all fields** | Create with name + prompt + repo_url + assigned_user_id → Get → all fields round-trip | +| 3 | **Pagination** | Create 5 sessions → List with size=2 → verify 3 pages → ListAll iterator returns all 5 | +| 4 | **Search and ordering** | Create sessions with distinct names → List with search filter → verify filtered results → OrderBy created_at desc | +| 5 | **Error: missing required field** | Create session without name → expect 400 with structured `APIError{code, reason}` | +| 6 | **Error: not found** | Get session with fake ID → expect 404 | +| 7 | **Error: bad auth** | Create client with invalid token → any request → expect 401 | +| 8 | **Project isolation** | Create session in project A → List in project B → session not visible | +| 9 | **Workflow lifecycle** | Create agent → Create workflow with agent_id → Get workflow → verify agent_id set | +| 10 | **WorkflowSkill/WorkflowTask** | Create skill → Create workflow → Create WorkflowSkill linking them → List WorkflowSkills → verify position | +| 11 | **User CRUD + DELETE** | Create user → Get → Patch → Delete → Get → 404 (only resource with DELETE) | +| 12 | **Control plane propagation** | Create session → poll until status changes from `pending` → verify CP picked it up and operator created a Job | +| 13 | **Builder validation client-side** | Verify builder rejects invalid input before HTTP call (no network round-trip for obvious errors) | +| 14 | **Concurrent creates** | 10 goroutines/threads create sessions simultaneously → all succeed → List returns all 10 | + +#### E2E Test Structure (Go) + +```go +func TestE2E(t *testing.T) { + client, err := ambient.NewClientFromEnv() + if err != nil { + t.Skipf("e2e: skipping, no live server: %v", err) + } + + t.Run("SessionLifecycle", func(t *testing.T) { + ctx := context.Background() + + session, err := client.Sessions().Create(ctx, + ambient.NewSessionBuilder(). + Name("e2e-lifecycle-"+randomSuffix()). + Prompt("e2e test session"). + Build(), + ) + require.NoError(t, err) + require.NotEmpty(t, session.ID) + + t.Cleanup(func() { + // best-effort cleanup — sessions don't have DELETE yet + }) + + got, err := client.Sessions().Get(ctx, session.ID) + require.NoError(t, err) + assert.Equal(t, session.Name, got.Name) + + list, err := client.Sessions().List(ctx, ambient.NewListOptions().Search("name='"+session.Name+"'").Build()) + require.NoError(t, err) + assert.GreaterOrEqual(t, list.Total, 1) + + updated, err := client.Sessions().Update(ctx, session.ID, + ambient.NewSessionPatchBuilder(). + Prompt("updated prompt"). + Build(), + ) + require.NoError(t, err) + assert.Equal(t, "updated prompt", updated.Prompt) + }) +} +``` + +#### E2E Test Structure (Python) + +```python +@pytest.fixture +def client(): + try: + c = AmbientClient.from_env() + except ValueError: + pytest.skip("e2e: no live server configured") + with c: + yield c + +def test_session_lifecycle(client): + name = f"e2e-lifecycle-{uuid4().hex[:8]}" + + session = client.sessions.create( + Session.builder() + .name(name) + .prompt("e2e test session") + .build() + ) + assert session.id + + got = client.sessions.get(session.id) + assert got.name == name + + sessions = client.sessions.list( + ListOptions().search(f"name='{name}'") + ) + assert sessions.total >= 1 + + updated = client.sessions.update(session.id, + SessionPatch().prompt("updated prompt") + ) + assert updated.prompt == "updated prompt" +``` + +#### CI Integration + +```yaml +# Unit tests (every PR) +- name: SDK unit tests + run: | + cd go-sdk && go test ./... + cd python-sdk && pytest tests/ -v + +# E2E tests (nightly or on-demand against staging) +- name: SDK e2e tests + if: github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'e2e') + env: + AMBIENT_API_URL: ${{ secrets.STAGING_API_URL }} + AMBIENT_TOKEN: ${{ secrets.STAGING_TOKEN }} + AMBIENT_PROJECT: e2e-test-project + run: | + cd e2e && go test -v -timeout 5m ./... + cd e2e && pytest e2e_test.py -v --timeout=300 +``` + +#### E2E as platform health check + +The e2e suite doubles as a **platform health check**. Running it against a live deployment answers: + +- Is the API server accepting requests? +- Is authentication working? +- Is project scoping enforced? +- Is the control plane picking up new sessions? +- Is the operator creating Jobs? +- Are all 4 resource types functioning? +- Is pagination correct? +- Are error responses structured correctly? + +This makes the SDK e2e suite the **baseline smoke test** that Overlord requested — run it before and after any deployment to verify the platform is healthy. + +## CI Integration + +The SDK generator is integrated into the CI/CD pipeline with automated testing and validation. + +### GitHub Actions Workflow + +The `ambient-sdk.yml` workflow handles: + +1. **Change Detection** - Only runs when SDK or OpenAPI files change +2. **Generator Validation** - Ensures the generator builds and produces valid output +3. **Multi-language Testing** - Tests Go, Python, and TypeScript SDKs in parallel +4. **Integration Testing** - Cross-language compatibility verification + +### Workflow Triggers + +```yaml +on: + push: + branches: [main] + paths: + - 'components/ambient-sdk/**' + - 'components/ambient-api-server/openapi/**' + pull_request: + branches: [main] + paths: + - 'components/ambient-sdk/**' + - 'components/ambient-api-server/openapi/**' +``` + +### Test Matrix + +| Language | Tests | Coverage | +|----------|-------|----------| +| Go | `go test ./...`, `golangci-lint`, build validation | Types, clients, examples | +| Python | `pytest`, `mypy`, `black`, `isort` | Types, clients, examples | +| TypeScript | `npm test`, `tsc --noEmit`, ESLint | Types, clients, examples | + +### Regeneration Detection + +The CI automatically detects when SDKs are out of sync with the OpenAPI specification: + +1. Runs the generator on the current spec +2. Compares output with committed SDK files +3. Fails if there are differences + +This prevents SDK drift and ensures developers regenerate SDKs when the API changes. + +### Environment Variables for Testing + +| Variable | Purpose | Example | +|----------|---------|---------| +| `AMBIENT_TOKEN` | Test authentication | `sha256~test-token` | +| `AMBIENT_PROJECT` | Test project scoping | `test-project` | +| `AMBIENT_API_URL` | Test API endpoint | `http://localhost:8080` | + +## Development Workflow + +### Making Changes + +1. **Update OpenAPI specification** in `components/ambient-api-server/openapi/` +2. **Regenerate SDKs** using the generator +3. **Run tests** to ensure compatibility +4. **Update examples** if APIs changed significantly + +### Adding New Resources + +1. **Add OpenAPI spec** for the new resource +2. **Update parser.go** to include the new resource file +3. **Regenerate all SDKs** - new types and clients are created automatically +4. **Add integration tests** for the new resource + +### Template Updates + +When modifying templates: + +1. **Test against current spec** to ensure valid output +2. **Run golden file tests** to verify format consistency +3. **Update documentation** if template behavior changes + +The generator's modular design ensures that updates to templates automatically apply to all current and future resources. diff --git a/components/ambient-sdk/generator/ambient-sdk-generator b/components/ambient-sdk/generator/ambient-sdk-generator new file mode 100755 index 000000000..7a773e64b Binary files /dev/null and b/components/ambient-sdk/generator/ambient-sdk-generator differ diff --git a/components/ambient-sdk/generator/go.mod b/components/ambient-sdk/generator/go.mod new file mode 100644 index 000000000..87dcdb0d6 --- /dev/null +++ b/components/ambient-sdk/generator/go.mod @@ -0,0 +1,5 @@ +module github.com/ambient-code/platform/components/ambient-sdk/generator + +go 1.24.4 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/components/ambient-sdk/generator/go.sum b/components/ambient-sdk/generator/go.sum new file mode 100644 index 000000000..a62c313c5 --- /dev/null +++ b/components/ambient-sdk/generator/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/components/ambient-sdk/generator/main.go b/components/ambient-sdk/generator/main.go new file mode 100644 index 000000000..ab9dc5fe1 --- /dev/null +++ b/components/ambient-sdk/generator/main.go @@ -0,0 +1,391 @@ +package main + +import ( + "crypto/sha256" + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "text/template" + "time" +) + +func main() { + specPath := flag.String("spec", "", "path to openapi.yaml") + goOut := flag.String("go-out", "", "output directory for Go SDK") + pythonOut := flag.String("python-out", "", "output directory for Python SDK") + tsOut := flag.String("ts-out", "", "output directory for TypeScript SDK") + flag.Parse() + + if *specPath == "" { + log.Fatal("--spec is required") + } + if *goOut == "" && *pythonOut == "" && *tsOut == "" { + log.Fatal("at least one of --go-out, --python-out, or --ts-out is required") + } + + spec, err := parseSpec(*specPath) + if err != nil { + log.Fatalf("parse spec: %v", err) + } + + specHash, err := computeSpecHash(*specPath) + if err != nil { + log.Fatalf("compute spec hash: %v", err) + } + + // Use relative path for spec source + relativeSpecPath := "../../ambient-api-server/openapi/openapi.yaml" + + header := GeneratedHeader{ + SpecPath: relativeSpecPath, + SpecHash: specHash, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + + fmt.Printf("Parsed %d resources from %s\n", len(spec.Resources), *specPath) + for _, r := range spec.Resources { + fmt.Printf(" %s (%s): %d fields, delete=%v\n", r.Name, r.PathSegment, len(r.Fields), r.HasDelete) + } + + if *goOut != "" { + if err := generateGo(spec, *goOut, header); err != nil { + log.Fatalf("generate Go: %v", err) + } + fmt.Printf("Go SDK generated in %s\n", *goOut) + } + + if *pythonOut != "" { + if err := generatePython(spec, *pythonOut, header); err != nil { + log.Fatalf("generate Python: %v", err) + } + fmt.Printf("Python SDK generated in %s\n", *pythonOut) + } + + if *tsOut != "" { + if err := generateTypeScript(spec, *tsOut, header); err != nil { + log.Fatalf("generate TypeScript: %v", err) + } + fmt.Printf("TypeScript SDK generated in %s\n", *tsOut) + } +} + +type GeneratedHeader struct { + SpecPath string + SpecHash string + Timestamp string +} + +type goTemplateData struct { + Header GeneratedHeader + Resource Resource + Spec *Spec +} + +type pythonTemplateData struct { + Header GeneratedHeader + Resource Resource + Spec *Spec +} + +type tsTemplateData struct { + Header GeneratedHeader + Resource Resource + Spec *Spec +} + +func generateGo(spec *Spec, outDir string, header GeneratedHeader) error { + typesDir := filepath.Join(outDir, "types") + clientDir := filepath.Join(outDir, "client") + if err := os.MkdirAll(typesDir, 0755); err != nil { + return err + } + if err := os.MkdirAll(clientDir, 0755); err != nil { + return err + } + + tmplDir := filepath.Join(getTemplateDir(), "go") + + baseTmpl, err := loadTemplate(filepath.Join(tmplDir, "base.go.tmpl")) + if err != nil { + return fmt.Errorf("load base template: %w", err) + } + if err := executeTemplate(baseTmpl, filepath.Join(typesDir, "base.go"), goTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute base template: %w", err) + } + + typesTmpl, err := loadTemplate(filepath.Join(tmplDir, "types.go.tmpl")) + if err != nil { + return fmt.Errorf("load types template: %w", err) + } + + clientTmpl, err := loadTemplate(filepath.Join(tmplDir, "client.go.tmpl")) + if err != nil { + return fmt.Errorf("load client template: %w", err) + } + + for _, r := range spec.Resources { + data := goTemplateData{Header: header, Resource: r, Spec: spec} + fileName := toSnakeCase(r.Name) + ".go" + + if err := executeTemplate(typesTmpl, filepath.Join(typesDir, fileName), data); err != nil { + return fmt.Errorf("execute types template for %s: %w", r.Name, err) + } + + apiFileName := toSnakeCase(r.Name) + "_api.go" + if err := executeTemplate(clientTmpl, filepath.Join(clientDir, apiFileName), data); err != nil { + return fmt.Errorf("execute client template for %s: %w", r.Name, err) + } + } + + iteratorTmpl, err := loadTemplate(filepath.Join(tmplDir, "iterator.go.tmpl")) + if err != nil { + return fmt.Errorf("load iterator template: %w", err) + } + if err := executeTemplate(iteratorTmpl, filepath.Join(clientDir, "iterator.go"), goTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute iterator template: %w", err) + } + + listOptsTmpl, err := loadTemplate(filepath.Join(tmplDir, "list_options.go.tmpl")) + if err != nil { + return fmt.Errorf("load list_options template: %w", err) + } + if err := executeTemplate(listOptsTmpl, filepath.Join(typesDir, "list_options.go"), goTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute list_options template: %w", err) + } + + // Generate main HTTP client + httpClientTmpl, err := loadTemplate(filepath.Join(tmplDir, "http_client.go.tmpl")) + if err != nil { + return fmt.Errorf("load http_client template: %w", err) + } + if err := executeTemplate(httpClientTmpl, filepath.Join(clientDir, "client.go"), goTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute http_client template: %w", err) + } + + return nil +} + +func generateTypeScript(spec *Spec, outDir string, header GeneratedHeader) error { + srcDir := filepath.Join(outDir, "src") + if err := os.MkdirAll(srcDir, 0755); err != nil { + return err + } + + tmplDir := filepath.Join(getTemplateDir(), "ts") + + baseTmpl, err := loadTemplate(filepath.Join(tmplDir, "base.ts.tmpl")) + if err != nil { + return fmt.Errorf("load base template: %w", err) + } + if err := executeTemplate(baseTmpl, filepath.Join(srcDir, "base.ts"), tsTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute base template: %w", err) + } + + typesTmpl, err := loadTemplate(filepath.Join(tmplDir, "types.ts.tmpl")) + if err != nil { + return fmt.Errorf("load types template: %w", err) + } + + clientTmpl, err := loadTemplate(filepath.Join(tmplDir, "client.ts.tmpl")) + if err != nil { + return fmt.Errorf("load client template: %w", err) + } + + for _, r := range spec.Resources { + data := tsTemplateData{Header: header, Resource: r, Spec: spec} + fileName := toSnakeCase(r.Name) + ".ts" + + if err := executeTemplate(typesTmpl, filepath.Join(srcDir, fileName), data); err != nil { + return fmt.Errorf("execute types template for %s: %w", r.Name, err) + } + + apiFileName := toSnakeCase(r.Name) + "_api.ts" + if err := executeTemplate(clientTmpl, filepath.Join(srcDir, apiFileName), data); err != nil { + return fmt.Errorf("execute client template for %s: %w", r.Name, err) + } + } + + ambientClientTmpl, err := loadTemplate(filepath.Join(tmplDir, "ambient_client.ts.tmpl")) + if err != nil { + return fmt.Errorf("load ambient_client template: %w", err) + } + if err := executeTemplate(ambientClientTmpl, filepath.Join(srcDir, "client.ts"), tsTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute ambient_client template: %w", err) + } + + indexTmpl, err := loadTemplate(filepath.Join(tmplDir, "index.ts.tmpl")) + if err != nil { + return fmt.Errorf("load index template: %w", err) + } + if err := executeTemplate(indexTmpl, filepath.Join(srcDir, "index.ts"), tsTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute index template: %w", err) + } + + return nil +} + +func generatePython(spec *Spec, outDir string, header GeneratedHeader) error { + pkgDir := outDir + if err := os.MkdirAll(pkgDir, 0755); err != nil { + return err + } + + tmplDir := filepath.Join(getTemplateDir(), "python") + + baseTmpl, err := loadTemplate(filepath.Join(tmplDir, "base.py.tmpl")) + if err != nil { + return fmt.Errorf("load base template: %w", err) + } + if err := executeTemplate(baseTmpl, filepath.Join(pkgDir, "_base.py"), pythonTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute base template: %w", err) + } + + typesTmpl, err := loadTemplate(filepath.Join(tmplDir, "types.py.tmpl")) + if err != nil { + return fmt.Errorf("load types template: %w", err) + } + + clientTmpl, err := loadTemplate(filepath.Join(tmplDir, "client.py.tmpl")) + if err != nil { + return fmt.Errorf("load client template: %w", err) + } + + for _, r := range spec.Resources { + data := pythonTemplateData{Header: header, Resource: r, Spec: spec} + fileName := toSnakeCase(r.Name) + ".py" + + if err := executeTemplate(typesTmpl, filepath.Join(pkgDir, fileName), data); err != nil { + return fmt.Errorf("execute types template for %s: %w", r.Name, err) + } + + apiFileName := "_" + toSnakeCase(r.Name) + "_api.py" + if err := executeTemplate(clientTmpl, filepath.Join(pkgDir, apiFileName), data); err != nil { + return fmt.Errorf("execute client template for %s: %w", r.Name, err) + } + } + + iteratorTmpl, err := loadTemplate(filepath.Join(tmplDir, "iterator.py.tmpl")) + if err != nil { + return fmt.Errorf("load iterator template: %w", err) + } + if err := executeTemplate(iteratorTmpl, filepath.Join(pkgDir, "_iterator.py"), pythonTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute iterator template: %w", err) + } + + // Generate main HTTP client + httpClientTmpl, err := loadTemplate(filepath.Join(tmplDir, "http_client.py.tmpl")) + if err != nil { + return fmt.Errorf("load http_client template: %w", err) + } + if err := executeTemplate(httpClientTmpl, filepath.Join(pkgDir, "client.py"), pythonTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute http_client template: %w", err) + } + + // Generate __init__.py + initTmpl, err := loadTemplate(filepath.Join(tmplDir, "__init__.py.tmpl")) + if err != nil { + return fmt.Errorf("load __init__.py template: %w", err) + } + if err := executeTemplate(initTmpl, filepath.Join(pkgDir, "__init__.py"), pythonTemplateData{Header: header, Spec: spec}); err != nil { + return fmt.Errorf("execute __init__.py template: %w", err) + } + + return nil +} + +func loadTemplate(path string) (*template.Template, error) { + funcMap := template.FuncMap{ + "snakeCase": toSnakeCase, + "lower": strings.ToLower, + "title": func(s string) string { + if s == "" { + return s + } + r := []rune(s) + r[0] = []rune(strings.ToUpper(string(r[0])))[0] + return string(r) + }, + "goName": toGoName, + "pythonDefault": func(f Field) string { return pythonDefault(f.Type, f.Format) }, + "isDateTime": isDateTimeField, + "isWritable": func(f Field) bool { return !f.ReadOnly }, + "camelCase": toCamelCase, + "pluralize": pluralize, + "lowerFirst": lowerFirst, + "tsDefault": func(f Field) string { return tsDefault(f.Type, f.Format) }, + "hasTimeImport": func(fields []Field) bool { + for _, f := range fields { + if f.Format == "date-time" { + return true + } + } + return false + }, + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + tmpl, err := template.New(filepath.Base(path)).Funcs(funcMap).Parse(string(data)) + if err != nil { + return nil, fmt.Errorf("parse template %s: %w", path, err) + } + + return tmpl, nil +} + +func executeTemplate(tmpl *template.Template, outPath string, data interface{}) error { + f, err := os.Create(outPath) + if err != nil { + return err + } + defer f.Close() + + return tmpl.Execute(f, data) +} + +func computeSpecHash(specPath string) (string, error) { + specDir := filepath.Dir(specPath) + h := sha256.New() + + files := []string{ + specPath, + filepath.Join(specDir, "openapi.sessions.yaml"), + filepath.Join(specDir, "openapi.projects.yaml"), + filepath.Join(specDir, "openapi.projectSettings.yaml"), + filepath.Join(specDir, "openapi.users.yaml"), + } + + for _, f := range files { + fh, err := os.Open(f) + if err != nil { + return "", fmt.Errorf("open %s: %w", f, err) + } + if _, err := io.Copy(h, fh); err != nil { + fh.Close() + return "", fmt.Errorf("read %s: %w", f, err) + } + fh.Close() + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +func getTemplateDir() string { + exe, err := os.Executable() + if err != nil { + return "templates" + } + dir := filepath.Dir(exe) + tmplDir := filepath.Join(dir, "templates") + if _, err := os.Stat(tmplDir); err == nil { + return tmplDir + } + return "templates" +} diff --git a/components/ambient-sdk/generator/model.go b/components/ambient-sdk/generator/model.go new file mode 100644 index 000000000..9a32dbf24 --- /dev/null +++ b/components/ambient-sdk/generator/model.go @@ -0,0 +1,235 @@ +package main + +import ( + "fmt" + "strings" + "unicode" +) + +type Resource struct { + Name string + Plural string + PathSegment string + Fields []Field + RequiredFields []string + PatchFields []Field + StatusPatchFields []Field + HasDelete bool + HasPatch bool + HasStatusPatch bool + Actions []string +} + +type Field struct { + Name string + GoName string + PythonName string + TSName string + Type string + Format string + GoType string + PythonType string + TSType string + Required bool + ReadOnly bool + JSONTag string +} + +type Spec struct { + Resources []Resource +} + +func toGoName(snakeName string) string { + parts := strings.Split(snakeName, "_") + var result strings.Builder + for _, part := range parts { + if part == "" { + continue + } + if upper, ok := commonAcronyms[strings.ToUpper(part)]; ok { + result.WriteString(upper) + continue + } + runes := []rune(part) + if len(runes) == 0 { + continue + } + runes[0] = unicode.ToUpper(runes[0]) + result.WriteString(string(runes)) + } + return result.String() +} + +var commonAcronyms = map[string]string{ + "ID": "ID", + "URL": "URL", + "HTTP": "HTTP", + "API": "API", + "UI": "UI", +} + +func toGoType(openAPIType, format string) string { + switch openAPIType { + case "string": + if format == "date-time" { + return "*time.Time" + } + return "string" + case "integer": + if format == "int32" { + return "int32" + } + return "int" + case "number": + if format == "double" || format == "float" { + return "float64" + } + return "float64" + case "boolean": + return "bool" + default: + return "string" + } +} + +func toPythonType(openAPIType, format string) string { + switch openAPIType { + case "string": + if format == "date-time" { + return "Optional[datetime]" + } + return "str" + case "integer": + return "int" + case "number": + return "float" + case "boolean": + return "bool" + default: + return "str" + } +} + +func pythonDefault(openAPIType, format string) string { + switch openAPIType { + case "string": + if format == "date-time" { + return "None" + } + return "\"\"" + case "integer": + return "0" + case "number": + return "0.0" + case "boolean": + return "False" + default: + return "\"\"" + } +} + +func jsonTag(name string, required bool) string { + if required { + return fmt.Sprintf("`json:\"%s\"`", name) + } + return fmt.Sprintf("`json:\"%s,omitempty\"`", name) +} + +func toTSType(openAPIType, format string) string { + switch openAPIType { + case "string": + if format == "date-time" { + return "string" + } + return "string" + case "integer": + return "number" + case "number": + return "number" + case "boolean": + return "boolean" + default: + return "string" + } +} + +func tsDefault(openAPIType, format string) string { + switch openAPIType { + case "string": + return "''" + case "integer": + return "0" + case "number": + return "0" + case "boolean": + return "false" + default: + return "''" + } +} + +func toCamelCase(snakeName string) string { + parts := strings.Split(snakeName, "_") + if len(parts) == 0 { + return snakeName + } + var result strings.Builder + result.WriteString(parts[0]) + for _, part := range parts[1:] { + if part == "" { + continue + } + runes := []rune(part) + runes[0] = unicode.ToUpper(runes[0]) + result.WriteString(string(runes)) + } + return result.String() +} + +func lowerFirst(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToLower(runes[0]) + return string(runes) +} + +func toSnakeCase(camelCase string) string { + var result strings.Builder + for i, r := range camelCase { + if unicode.IsUpper(r) && i > 0 { + result.WriteRune('_') + } + result.WriteRune(unicode.ToLower(r)) + } + return result.String() +} + +func pluralize(name string) string { + lower := strings.ToLower(name) + + // Handle already-plural compound words + exceptions := map[string]string{ + "project_settings": "project_settings", + "projectsettings": "project_settings", + } + + if plural, exists := exceptions[lower]; exists { + return plural + } + + // Check for already plural words ending in settings, data, etc. + if strings.HasSuffix(lower, "settings") || strings.HasSuffix(lower, "data") || + strings.HasSuffix(lower, "metadata") || strings.HasSuffix(lower, "info") { + return lower + } + + if strings.HasSuffix(lower, "s") { + return lower + "es" + } + if strings.HasSuffix(lower, "y") { + return lower[:len(lower)-1] + "ies" + } + return lower + "s" +} diff --git a/components/ambient-sdk/generator/parser.go b/components/ambient-sdk/generator/parser.go new file mode 100644 index 000000000..2e5857481 --- /dev/null +++ b/components/ambient-sdk/generator/parser.go @@ -0,0 +1,381 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "gopkg.in/yaml.v3" +) + +type openAPIDoc struct { + Paths map[string]interface{} `yaml:"paths"` + Components struct { + Schemas map[string]interface{} `yaml:"schemas"` + } `yaml:"components"` +} + +type subSpecDoc struct { + Paths map[string]interface{} `yaml:"paths"` + Components struct { + Schemas map[string]interface{} `yaml:"schemas"` + } `yaml:"components"` +} + +func parseSpec(specPath string) (*Spec, error) { + mainData, err := os.ReadFile(specPath) + if err != nil { + return nil, fmt.Errorf("read spec: %w", err) + } + + var mainDoc openAPIDoc + if err := yaml.Unmarshal(mainData, &mainDoc); err != nil { + return nil, fmt.Errorf("parse main spec: %w", err) + } + + specDir := filepath.Dir(specPath) + + resourceFiles := map[string]string{ + "Session": "openapi.sessions.yaml", + "User": "openapi.users.yaml", + "Project": "openapi.projects.yaml", + "ProjectSettings": "openapi.projectSettings.yaml", + } + + pathSegments := map[string]string{ + "Session": "sessions", + "User": "users", + "Project": "projects", + "ProjectSettings": "project_settings", + } + + var resources []Resource + for name, file := range resourceFiles { + subPath := filepath.Join(specDir, file) + subData, err := os.ReadFile(subPath) + if err != nil { + return nil, fmt.Errorf("read sub-spec %s: %w", file, err) + } + + var subDoc subSpecDoc + if err := yaml.Unmarshal(subData, &subDoc); err != nil { + return nil, fmt.Errorf("parse sub-spec %s: %w", file, err) + } + + resource, err := extractResource(name, pathSegments[name], &subDoc) + if err != nil { + return nil, fmt.Errorf("extract resource %s: %w", name, err) + } + + resources = append(resources, *resource) + } + + sort.Slice(resources, func(i, j int) bool { + return resources[i].Name < resources[j].Name + }) + + return &Spec{Resources: resources}, nil +} + +func extractResource(name, pathSegment string, doc *subSpecDoc) (*Resource, error) { + schema, ok := doc.Components.Schemas[name] + if !ok { + return nil, fmt.Errorf("schema %s not found", name) + } + + schemaMap, ok := schema.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("schema %s is not a map", name) + } + + fields, requiredFields, err := extractFields(schemaMap) + if err != nil { + return nil, fmt.Errorf("extract fields for %s: %w", name, err) + } + + patchName := name + "PatchRequest" + patchSchema, ok := doc.Components.Schemas[patchName] + var patchFields []Field + if ok { + patchMap, ok := patchSchema.(map[string]interface{}) + if ok { + patchFields, _, err = extractPatchFields(patchMap) + if err != nil { + return nil, fmt.Errorf("extract patch fields for %s: %w", name, err) + } + } + } + + statusPatchName := name + "StatusPatchRequest" + statusPatchSchema, ok := doc.Components.Schemas[statusPatchName] + var statusPatchFields []Field + hasStatusPatch := false + if ok { + statusPatchMap, ok := statusPatchSchema.(map[string]interface{}) + if ok { + statusPatchFields, _, err = extractPatchFields(statusPatchMap) + if err != nil { + return nil, fmt.Errorf("extract status patch fields for %s: %w", name, err) + } + hasStatusPatch = len(statusPatchFields) > 0 + } + } + + hasDelete := checkHasDelete(doc.Paths, pathSegment) + hasPatch := checkHasPatch(doc.Paths, pathSegment) + actions := detectActions(doc.Paths, pathSegment) + + return &Resource{ + Name: name, + Plural: resourcePlural(name), + PathSegment: pathSegment, + Fields: fields, + RequiredFields: requiredFields, + PatchFields: patchFields, + StatusPatchFields: statusPatchFields, + HasDelete: hasDelete, + HasPatch: hasPatch, + HasStatusPatch: hasStatusPatch, + Actions: actions, + }, nil +} + +func resourcePlural(name string) string { + switch name { + case "Session": + return "Sessions" + case "Agent": + return "Agents" + case "Task": + return "Tasks" + case "Skill": + return "Skills" + case "Workflow": + return "Workflows" + case "User": + return "Users" + case "WorkflowSkill": + return "WorkflowSkills" + case "WorkflowTask": + return "WorkflowTasks" + case "Project": + return "Projects" + case "ProjectSettings": + return "ProjectSettings" + case "Permission": + return "Permissions" + case "RepositoryRef": + return "RepositoryRefs" + case "ProjectKey": + return "ProjectKeys" + default: + return name + "s" + } +} + +func extractFields(schemaMap map[string]interface{}) ([]Field, []string, error) { + allOf, ok := schemaMap["allOf"] + if !ok { + return nil, nil, fmt.Errorf("schema missing allOf") + } + + allOfList, ok := allOf.([]interface{}) + if !ok { + return nil, nil, fmt.Errorf("allOf is not a list") + } + + var fields []Field + var requiredFields []string + + for _, item := range allOfList { + itemMap, ok := item.(map[string]interface{}) + if !ok { + continue + } + + if _, hasRef := itemMap["$ref"]; hasRef { + continue + } + + if req, ok := itemMap["required"]; ok { + if reqList, ok := req.([]interface{}); ok { + for _, r := range reqList { + if s, ok := r.(string); ok { + requiredFields = append(requiredFields, s) + } + } + } + } + + props, ok := itemMap["properties"] + if !ok { + continue + } + + propsMap, ok := props.(map[string]interface{}) + if !ok { + continue + } + + for propName, propVal := range propsMap { + if isObjectReferenceField(propName) { + continue + } + + propMap, ok := propVal.(map[string]interface{}) + if !ok { + continue + } + + propType, _ := propMap["type"].(string) + propFormat, _ := propMap["format"].(string) + readOnly, _ := propMap["readOnly"].(bool) + + isRequired := false + for _, r := range requiredFields { + if r == propName { + isRequired = true + break + } + } + + f := Field{ + Name: propName, + GoName: toGoName(propName), + PythonName: propName, + TSName: toCamelCase(propName), + Type: propType, + Format: propFormat, + GoType: toGoType(propType, propFormat), + PythonType: toPythonType(propType, propFormat), + TSType: toTSType(propType, propFormat), + Required: isRequired, + ReadOnly: readOnly, + JSONTag: jsonTag(propName, isRequired), + } + + fields = append(fields, f) + } + } + + sort.Slice(fields, func(i, j int) bool { + return fields[i].Name < fields[j].Name + }) + + return fields, requiredFields, nil +} + +func extractPatchFields(schemaMap map[string]interface{}) ([]Field, []string, error) { + props, ok := schemaMap["properties"] + if !ok { + return nil, nil, nil + } + + propsMap, ok := props.(map[string]interface{}) + if !ok { + return nil, nil, nil + } + + var fields []Field + for propName, propVal := range propsMap { + propMap, ok := propVal.(map[string]interface{}) + if !ok { + continue + } + + propType, _ := propMap["type"].(string) + propFormat, _ := propMap["format"].(string) + + f := Field{ + Name: propName, + GoName: toGoName(propName), + PythonName: propName, + TSName: toCamelCase(propName), + Type: propType, + Format: propFormat, + GoType: toGoType(propType, propFormat), + PythonType: toPythonType(propType, propFormat), + TSType: toTSType(propType, propFormat), + Required: false, + JSONTag: jsonTag(propName, false), + } + + fields = append(fields, f) + } + + sort.Slice(fields, func(i, j int) bool { + return fields[i].Name < fields[j].Name + }) + + return fields, nil, nil +} + +func checkHasPatch(paths map[string]interface{}, pathSegment string) bool { + idPath := fmt.Sprintf("/api/ambient-api-server/v1/%s/{id}", pathSegment) + pathVal, ok := paths[idPath] + if !ok { + return false + } + + pathMap, ok := pathVal.(map[string]interface{}) + if !ok { + return false + } + + _, hasPatch := pathMap["patch"] + return hasPatch +} + +func checkHasDelete(paths map[string]interface{}, pathSegment string) bool { + idPath := fmt.Sprintf("/api/ambient-api-server/v1/%s/{id}", pathSegment) + pathVal, ok := paths[idPath] + if !ok { + return false + } + + pathMap, ok := pathVal.(map[string]interface{}) + if !ok { + return false + } + + _, hasDelete := pathMap["delete"] + return hasDelete +} + +func detectActions(paths map[string]interface{}, pathSegment string) []string { + knownActions := []string{"start", "stop"} + var found []string + for _, action := range knownActions { + actionPath := fmt.Sprintf("/api/ambient-api-server/v1/%s/{id}/%s", pathSegment, action) + pathVal, ok := paths[actionPath] + if !ok { + continue + } + pathMap, ok := pathVal.(map[string]interface{}) + if !ok { + continue + } + if _, hasPost := pathMap["post"]; hasPost { + found = append(found, action) + } + } + return found +} + +var objectReferenceFields = map[string]bool{ + "id": true, + "kind": true, + "href": true, + "created_at": true, + "updated_at": true, +} + +func isObjectReferenceField(name string) bool { + return objectReferenceFields[name] +} + +func isDateTimeField(f Field) bool { + return f.Format == "date-time" && strings.Contains(f.GoType, "time.Time") +} diff --git a/components/ambient-sdk/generator/templates/go/base.go.tmpl b/components/ambient-sdk/generator/templates/go/base.go.tmpl new file mode 100644 index 000000000..296cf5a23 --- /dev/null +++ b/components/ambient-sdk/generator/templates/go/base.go.tmpl @@ -0,0 +1,43 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +package types + +import ( + "strconv" + "time" +) + +type ObjectReference struct { + ID string `json:"id,omitempty"` + Kind string `json:"kind,omitempty"` + Href string `json:"href,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +type ListMeta struct { + Kind string `json:"kind"` + Page int `json:"page"` + Size int `json:"size"` + Total int `json:"total"` +} + +type APIError struct { + ID string `json:"id,omitempty"` + Kind string `json:"kind,omitempty"` + Href string `json:"href,omitempty"` + Code string `json:"code"` + Reason string `json:"reason"` + OperationID string `json:"operation_id,omitempty"` + StatusCode int `json:"-"` +} + +func (e *APIError) Error() string { + if e.StatusCode > 0 { + return "ambient API error " + strconv.Itoa(e.StatusCode) + ": " + e.Code + " — " + e.Reason + } + return "ambient API error: " + e.Code + " — " + e.Reason +} diff --git a/components/ambient-sdk/generator/templates/go/client.go.tmpl b/components/ambient-sdk/generator/templates/go/client.go.tmpl new file mode 100644 index 000000000..862a2141e --- /dev/null +++ b/components/ambient-sdk/generator/templates/go/client.go.tmpl @@ -0,0 +1,100 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +type {{.Resource.Name}}API struct { + client *Client +} + +func (c *Client) {{.Resource.Plural}}() *{{.Resource.Name}}API { + return &{{.Resource.Name}}API{client: c} +} + +func (a *{{.Resource.Name}}API) Create(ctx context.Context, resource *types.{{.Resource.Name}}) (*types.{{.Resource.Name}}, error) { + body, err := json.Marshal(resource) + if err != nil { + return nil, fmt.Errorf("marshal {{.Resource.Name | snakeCase}}: %w", err) + } + var result types.{{.Resource.Name}} + if err := a.client.do(ctx, http.MethodPost, "/{{.Resource.PathSegment}}", body, http.StatusCreated, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *{{.Resource.Name}}API) Get(ctx context.Context, id string) (*types.{{.Resource.Name}}, error) { + var result types.{{.Resource.Name}} + if err := a.client.do(ctx, http.MethodGet, "/{{.Resource.PathSegment}}/"+url.PathEscape(id), nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *{{.Resource.Name}}API) List(ctx context.Context, opts *types.ListOptions) (*types.{{.Resource.Name}}List, error) { + var result types.{{.Resource.Name}}List + if err := a.client.doWithQuery(ctx, http.MethodGet, "/{{.Resource.PathSegment}}", nil, http.StatusOK, &result, opts); err != nil { + return nil, err + } + return &result, nil +} + +{{- if .Resource.HasPatch}} +func (a *{{.Resource.Name}}API) Update(ctx context.Context, id string, patch map[string]any) (*types.{{.Resource.Name}}, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal patch: %w", err) + } + var result types.{{.Resource.Name}} + if err := a.client.do(ctx, http.MethodPatch, "/{{.Resource.PathSegment}}/"+url.PathEscape(id), body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} +{{end}} +{{- if .Resource.HasDelete}} +func (a *{{.Resource.Name}}API) Delete(ctx context.Context, id string) error { + return a.client.do(ctx, http.MethodDelete, "/{{.Resource.PathSegment}}/"+url.PathEscape(id), nil, http.StatusNoContent, nil) +} +{{end}} +{{- if .Resource.HasStatusPatch}} +func (a *{{.Resource.Name}}API) UpdateStatus(ctx context.Context, id string, patch map[string]any) (*types.{{.Resource.Name}}, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal status patch: %w", err) + } + var result types.{{.Resource.Name}} + if err := a.client.do(ctx, http.MethodPatch, "/{{.Resource.PathSegment}}/"+url.PathEscape(id)+"/status", body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} +{{end}} +{{- range .Resource.Actions}} +func (a *{{$.Resource.Name}}API) {{. | title}}(ctx context.Context, id string) (*types.{{$.Resource.Name}}, error) { + var result types.{{$.Resource.Name}} + if err := a.client.do(ctx, http.MethodPost, "/{{$.Resource.PathSegment}}/"+url.PathEscape(id)+"/{{.}}", nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} +{{end}} +func (a *{{.Resource.Name}}API) ListAll(ctx context.Context, opts *types.ListOptions) *Iterator[types.{{.Resource.Name}}] { + return NewIterator(func(page int) (*types.{{.Resource.Name}}List, error) { + o := *opts + o.Page = page + return a.List(ctx, &o) + }) +} diff --git a/components/ambient-sdk/generator/templates/go/http_client.go.tmpl b/components/ambient-sdk/generator/templates/go/http_client.go.tmpl new file mode 100644 index 000000000..9673ef1f6 --- /dev/null +++ b/components/ambient-sdk/generator/templates/go/http_client.go.tmpl @@ -0,0 +1,232 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "time" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +type Client struct { + httpClient *http.Client + baseURL string + token string + project string + logger *slog.Logger + userAgent string +} + +type ClientOption func(*Client) + +func WithTimeout(timeout time.Duration) ClientOption { + return func(c *Client) { + c.httpClient.Timeout = timeout + } +} + +func WithLogger(logger *slog.Logger) ClientOption { + return func(c *Client) { + c.logger = logger + } +} + +func WithUserAgent(userAgent string) ClientOption { + return func(c *Client) { + c.userAgent = userAgent + } +} + +func NewClient(baseURL, token, project string, opts ...ClientOption) (*Client, error) { + if token == "" { + return nil, fmt.Errorf("token is required") + } + + if len(token) < 20 { + return nil, fmt.Errorf("token is too short (minimum 20 characters)") + } + + if token == "YOUR_TOKEN_HERE" || token == "PLACEHOLDER_TOKEN" { + return nil, fmt.Errorf("placeholder token is not allowed") + } + + if project == "" { + return nil, fmt.Errorf("project is required") + } + + if len(project) > 63 { + return nil, fmt.Errorf("project name cannot exceed 63 characters") + } + + if err := validateURL(baseURL); err != nil { + return nil, fmt.Errorf("invalid base URL: %w", err) + } + + c := &Client{ + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + baseURL: strings.TrimSuffix(baseURL, "/"), + token: token, + project: project, + logger: slog.Default(), + userAgent: "ambient-go-sdk/1.0.0", + } + + for _, opt := range opts { + opt(c) + } + + c.logger = c.logger.With(slog.String("sdk", "go"), slog.String("project", project)) + + return c, nil +} + +func NewClientFromEnv(opts ...ClientOption) (*Client, error) { + baseURL := os.Getenv("AMBIENT_API_URL") + if baseURL == "" { + baseURL = "http://localhost:8080" + } + + token := os.Getenv("AMBIENT_TOKEN") + if token == "" { + return nil, fmt.Errorf("AMBIENT_TOKEN environment variable is required") + } + + project := os.Getenv("AMBIENT_PROJECT") + if project == "" { + return nil, fmt.Errorf("AMBIENT_PROJECT environment variable is required") + } + + return NewClient(baseURL, token, project, opts...) +} + +func (c *Client) do(ctx context.Context, method, path string, body []byte, expectedStatus int, result interface{}) error { + url := c.baseURL + "/api/ambient-api-server/v1" + path + + req, err := http.NewRequestWithContext(ctx, method, url, nil) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + + if body != nil { + req.Body = io.NopCloser(bytes.NewReader(body)) + req.ContentLength = int64(len(body)) + req.Header.Set("Content-Type", "application/json") + } + + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("X-Ambient-Project", c.project) + req.Header.Set("User-Agent", c.userAgent) + req.Header.Set("Accept", "application/json") + + c.logger.Debug("HTTP request", + slog.String("method", method), + slog.String("url", sanitizeLogURL(url)), + slog.Int("body_len", len(body)), + ) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response body: %w", err) + } + + c.logger.Debug("HTTP response", + slog.Int("status", resp.StatusCode), + slog.Int("body_len", len(respBody)), + ) + + if resp.StatusCode != expectedStatus { + var apiErr types.APIError + if json.Unmarshal(respBody, &apiErr) == nil && apiErr.Code != "" { + apiErr.StatusCode = resp.StatusCode + return &apiErr + } + return &types.APIError{ + StatusCode: resp.StatusCode, + Code: "http_error", + Reason: fmt.Sprintf("HTTP %d: unexpected status", resp.StatusCode), + } + } + + if result != nil && len(respBody) > 0 { + if err := json.Unmarshal(respBody, result); err != nil { + return fmt.Errorf("unmarshal response: %w", err) + } + } + + return nil +} + +func (c *Client) doWithQuery(ctx context.Context, method, path string, body []byte, expectedStatus int, result interface{}, opts *types.ListOptions) error { + queryPath := path + if opts != nil { + params := url.Values{} + if opts.Page > 0 { + params.Set("page", fmt.Sprintf("%d", opts.Page)) + } + if opts.Size > 0 { + params.Set("size", fmt.Sprintf("%d", opts.Size)) + } + if opts.Search != "" { + params.Set("search", opts.Search) + } + if opts.OrderBy != "" { + params.Set("orderBy", opts.OrderBy) + } + if opts.Fields != "" { + params.Set("fields", opts.Fields) + } + if len(params) > 0 { + queryPath += "?" + params.Encode() + } + } + + return c.do(ctx, method, queryPath, body, expectedStatus, result) +} + +func validateURL(rawURL string) error { + if rawURL == "" { + return fmt.Errorf("URL cannot be empty") + } + + if strings.Contains(rawURL, "example.com") || strings.Contains(rawURL, "placeholder") { + return fmt.Errorf("placeholder URLs are not allowed") + } + + u, err := url.Parse(rawURL) + if err != nil { + return fmt.Errorf("invalid URL format: %w", err) + } + + if u.Scheme != "http" && u.Scheme != "https" { + return fmt.Errorf("only HTTP and HTTPS schemes are supported") + } + + return nil +} + +func sanitizeLogURL(rawURL string) string { + tokenPattern := regexp.MustCompile(`([Bb]earer\s+)[a-zA-Z0-9\-_~.+/=]+`) + return tokenPattern.ReplaceAllString(rawURL, "${1}[REDACTED]") +} \ No newline at end of file diff --git a/components/ambient-sdk/generator/templates/go/iterator.go.tmpl b/components/ambient-sdk/generator/templates/go/iterator.go.tmpl new file mode 100644 index 000000000..4ee5acc93 --- /dev/null +++ b/components/ambient-sdk/generator/templates/go/iterator.go.tmpl @@ -0,0 +1,65 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +package client + +type Listable[T any] interface { + GetItems() []T + GetTotal() int + GetPage() int + GetSize() int +} + +type Iterator[T any] struct { + fetchPage func(page int) (Listable[T], error) + items []T + index int + page int + total int + done bool + err error +} + +func NewIterator[T any, L Listable[T]](fetchPage func(page int) (L, error)) *Iterator[T] { + return &Iterator[T]{ + fetchPage: func(page int) (Listable[T], error) { + return fetchPage(page) + }, + page: 0, + index: -1, + } +} + +func (it *Iterator[T]) Next() bool { + if it.done || it.err != nil { + return false + } + it.index++ + if it.index < len(it.items) { + return true + } + it.page++ + result, err := it.fetchPage(it.page) + if err != nil { + it.err = err + return false + } + it.items = result.GetItems() + it.index = 0 + it.total = result.GetTotal() + if len(it.items) == 0 { + it.done = true + return false + } + return true +} + +func (it *Iterator[T]) Item() T { + return it.items[it.index] +} + +func (it *Iterator[T]) Err() error { + return it.err +} diff --git a/components/ambient-sdk/generator/templates/go/list_options.go.tmpl b/components/ambient-sdk/generator/templates/go/list_options.go.tmpl new file mode 100644 index 000000000..d60e05b95 --- /dev/null +++ b/components/ambient-sdk/generator/templates/go/list_options.go.tmpl @@ -0,0 +1,54 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +package types + +type ListOptions struct { + Page int + Size int + Search string + OrderBy string + Fields string +} + +type ListOptionsBuilder struct { + opts ListOptions +} + +func NewListOptions() *ListOptionsBuilder { + return &ListOptionsBuilder{opts: ListOptions{Page: 1, Size: 100}} +} + +func (b *ListOptionsBuilder) Page(page int) *ListOptionsBuilder { + b.opts.Page = page + return b +} + +func (b *ListOptionsBuilder) Size(size int) *ListOptionsBuilder { + if size > 65500 { + size = 65500 + } + b.opts.Size = size + return b +} + +func (b *ListOptionsBuilder) Search(search string) *ListOptionsBuilder { + b.opts.Search = search + return b +} + +func (b *ListOptionsBuilder) OrderBy(orderBy string) *ListOptionsBuilder { + b.opts.OrderBy = orderBy + return b +} + +func (b *ListOptionsBuilder) Fields(fields string) *ListOptionsBuilder { + b.opts.Fields = fields + return b +} + +func (b *ListOptionsBuilder) Build() *ListOptions { + return &b.opts +} diff --git a/components/ambient-sdk/generator/templates/go/types.go.tmpl b/components/ambient-sdk/generator/templates/go/types.go.tmpl new file mode 100644 index 000000000..4d9e02774 --- /dev/null +++ b/components/ambient-sdk/generator/templates/go/types.go.tmpl @@ -0,0 +1,104 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +package types + +import ( + "errors" + "fmt" +{{- if hasTimeImport .Resource.Fields}} + "time" +{{- end}} +) + +type {{.Resource.Name}} struct { + ObjectReference +{{range .Resource.Fields}} + {{.GoName}} {{.GoType}} {{.JSONTag}} +{{- end}} +} + +type {{.Resource.Name}}List struct { + ListMeta + Items []{{.Resource.Name}} `json:"items"` +} + +func (l *{{.Resource.Name}}List) GetItems() []{{.Resource.Name}} { return l.Items } +func (l *{{.Resource.Name}}List) GetTotal() int { return l.Total } +func (l *{{.Resource.Name}}List) GetPage() int { return l.Page } +func (l *{{.Resource.Name}}List) GetSize() int { return l.Size } + +type {{.Resource.Name}}Builder struct { + resource {{.Resource.Name}} + errors []error +} + +func New{{.Resource.Name}}Builder() *{{.Resource.Name}}Builder { + return &{{.Resource.Name}}Builder{} +} + +{{range .Resource.Fields}}{{if isWritable .}} +func (b *{{$.Resource.Name}}Builder) {{.GoName}}(v {{.GoType}}) *{{$.Resource.Name}}Builder { + b.resource.{{.GoName}} = v + return b +} +{{end}}{{end}} + +func (b *{{.Resource.Name}}Builder) Build() (*{{.Resource.Name}}, error) { +{{- range .Resource.Fields}} + {{- if .Required}} + {{- if eq .GoType "string"}} + if b.resource.{{.GoName}} == "" { + b.errors = append(b.errors, fmt.Errorf("{{.Name}} is required")) + } + {{- else if eq .GoType "int32"}} + {{- else if eq .GoType "int"}} + {{- end}} + {{- end}} +{{- end}} + if len(b.errors) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...)) + } + return &b.resource, nil +} + +type {{.Resource.Name}}PatchBuilder struct { + patch map[string]any +} + +func New{{.Resource.Name}}PatchBuilder() *{{.Resource.Name}}PatchBuilder { + return &{{.Resource.Name}}PatchBuilder{patch: make(map[string]any)} +} + +{{range .Resource.PatchFields}} +func (b *{{$.Resource.Name}}PatchBuilder) {{.GoName}}(v {{.GoType}}) *{{$.Resource.Name}}PatchBuilder { + b.patch["{{.Name}}"] = v + return b +} +{{end}} + +func (b *{{.Resource.Name}}PatchBuilder) Build() map[string]any { + return b.patch +} +{{if .Resource.HasStatusPatch}} +type {{.Resource.Name}}StatusPatchBuilder struct { + patch map[string]any +} + +func New{{.Resource.Name}}StatusPatchBuilder() *{{.Resource.Name}}StatusPatchBuilder { + return &{{.Resource.Name}}StatusPatchBuilder{patch: make(map[string]any)} +} + +{{range .Resource.StatusPatchFields}} +func (b *{{$.Resource.Name}}StatusPatchBuilder) {{.GoName}}(v {{.GoType}}) *{{$.Resource.Name}}StatusPatchBuilder { + b.patch["{{.Name}}"] = v + return b +} +{{end}} + +func (b *{{.Resource.Name}}StatusPatchBuilder) Build() map[string]any { + return b.patch +} +{{end}} diff --git a/components/ambient-sdk/generator/templates/python/__init__.py.tmpl b/components/ambient-sdk/generator/templates/python/__init__.py.tmpl new file mode 100644 index 000000000..03c6baaf3 --- /dev/null +++ b/components/ambient-sdk/generator/templates/python/__init__.py.tmpl @@ -0,0 +1,30 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: {{.Header.SpecPath}} +# Spec SHA256: {{.Header.SpecHash}} +# Generated: {{.Header.Timestamp}} + +"""Ambient Platform SDK for Python.""" + +from .client import AmbientClient +from ._base import APIError, ListOptions + +{{- range .Spec.Resources}} +from .{{.Name | snakeCase}} import {{.Name}}{{if .HasPatch}}, {{.Name}}Patch{{end}}{{if .HasStatusPatch}}, {{.Name}}StatusPatch{{end}} +{{- end}} + +__version__ = "1.0.0" + +__all__ = [ + "AmbientClient", + "APIError", + "ListOptions", +{{- range .Spec.Resources}} + "{{.Name}}", +{{- if .HasPatch}} + "{{.Name}}Patch", +{{- end}} +{{- if .HasStatusPatch}} + "{{.Name}}StatusPatch", +{{- end}} +{{- end}} +] \ No newline at end of file diff --git a/components/ambient-sdk/generator/templates/python/base.py.tmpl b/components/ambient-sdk/generator/templates/python/base.py.tmpl new file mode 100644 index 000000000..7871292b1 --- /dev/null +++ b/components/ambient-sdk/generator/templates/python/base.py.tmpl @@ -0,0 +1,106 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: {{.Header.SpecPath}} +# Spec SHA256: {{.Header.SpecHash}} +# Generated: {{.Header.Timestamp}} + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Optional + + +def _parse_datetime(value: Any) -> Optional[datetime]: + if value is None: + return None + if isinstance(value, datetime): + return value + if isinstance(value, str): + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + except (ValueError, TypeError): + return None + return None + + +@dataclass(frozen=True) +class ObjectReference: + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + @classmethod + def from_dict(cls, data: dict) -> ObjectReference: + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), + ) + + +@dataclass(frozen=True) +class ListMeta: + kind: str = "" + page: int = 0 + size: int = 0 + total: int = 0 + + +@dataclass(frozen=True) +class APIError(Exception): + status_code: int = 0 + code: str = "" + reason: str = "" + operation_id: str = "" + id: str = "" + kind: str = "" + href: str = "" + + def __str__(self) -> str: + return f"ambient API error {self.status_code}: {self.code} — {self.reason}" + + @classmethod + def from_dict(cls, data: dict, status_code: int = 0) -> APIError: + return cls( + status_code=status_code, + code=data.get("code", ""), + reason=data.get("reason", ""), + operation_id=data.get("operation_id", ""), + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + ) + + +class ListOptions: + def __init__(self) -> None: + self._params: dict[str, Any] = {"page": 1, "size": 100} + + def page(self, value: int) -> ListOptions: + self._params["page"] = value + return self + + def size(self, value: int) -> ListOptions: + if value > 65500: + value = 65500 + self._params["size"] = value + return self + + def search(self, value: str) -> ListOptions: + self._params["search"] = value + return self + + def order_by(self, value: str) -> ListOptions: + self._params["orderBy"] = value + return self + + def fields(self, value: str) -> ListOptions: + self._params["fields"] = value + return self + + def to_params(self) -> dict[str, Any]: + return {k: v for k, v in self._params.items() if v} diff --git a/components/ambient-sdk/generator/templates/python/client.py.tmpl b/components/ambient-sdk/generator/templates/python/client.py.tmpl new file mode 100644 index 000000000..a1157af54 --- /dev/null +++ b/components/ambient-sdk/generator/templates/python/client.py.tmpl @@ -0,0 +1,62 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: {{.Header.SpecPath}} +# Spec SHA256: {{.Header.SpecHash}} +# Generated: {{.Header.Timestamp}} + +from __future__ import annotations + +from typing import Any, Iterator, Optional, TYPE_CHECKING + +from ._base import ListOptions +from .{{.Resource.Name | snakeCase}} import {{.Resource.Name}}, {{.Resource.Name}}List{{if .Resource.HasStatusPatch}}, {{.Resource.Name}}StatusPatch{{end}} + +if TYPE_CHECKING: + from .client import AmbientClient + + +class {{.Resource.Name}}API: + def __init__(self, client: AmbientClient) -> None: + self._client = client + + def create(self, data: dict) -> {{.Resource.Name}}: + resp = self._client._request("POST", "/{{.Resource.PathSegment}}", json=data) + return {{.Resource.Name}}.from_dict(resp) + + def get(self, resource_id: str) -> {{.Resource.Name}}: + resp = self._client._request("GET", f"/{{.Resource.PathSegment}}/{resource_id}") + return {{.Resource.Name}}.from_dict(resp) + + def list(self, opts: Optional[ListOptions] = None) -> {{.Resource.Name}}List: + params = opts.to_params() if opts else None + resp = self._client._request("GET", "/{{.Resource.PathSegment}}", params=params) + return {{.Resource.Name}}List.from_dict(resp) + +{{- if .Resource.HasPatch}} + def update(self, resource_id: str, patch: Any) -> {{.Resource.Name}}: + data = patch.to_dict() if hasattr(patch, "to_dict") else patch + resp = self._client._request("PATCH", f"/{{.Resource.PathSegment}}/{resource_id}", json=data) + return {{.Resource.Name}}.from_dict(resp) +{{end}} +{{- if .Resource.HasDelete}} + def delete(self, resource_id: str) -> None: + self._client._request("DELETE", f"/{{.Resource.PathSegment}}/{resource_id}", expect_json=False) +{{end}} +{{- if .Resource.HasStatusPatch}} + def update_status(self, resource_id: str, patch: Any) -> {{.Resource.Name}}: + data = patch.to_dict() if hasattr(patch, "to_dict") else patch + resp = self._client._request("PATCH", f"/{{.Resource.PathSegment}}/{resource_id}/status", json=data) + return {{.Resource.Name}}.from_dict(resp) +{{end}} +{{- range .Resource.Actions}} + def {{.}}(self, resource_id: str) -> {{$.Resource.Name}}: + resp = self._client._request("POST", f"/{{$.Resource.PathSegment}}/{resource_id}/{{.}}") + return {{$.Resource.Name}}.from_dict(resp) +{{end}} + def list_all(self, size: int = 100, **kwargs: Any) -> Iterator[{{.Resource.Name}}]: + page = 1 + while True: + result = self.list(ListOptions().page(page).size(size)) + yield from result.items + if page * size >= result.total: + break + page += 1 diff --git a/components/ambient-sdk/generator/templates/python/http_client.py.tmpl b/components/ambient-sdk/generator/templates/python/http_client.py.tmpl new file mode 100644 index 000000000..af184d93a --- /dev/null +++ b/components/ambient-sdk/generator/templates/python/http_client.py.tmpl @@ -0,0 +1,179 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: {{.Header.SpecPath}} +# Spec SHA256: {{.Header.SpecHash}} +# Generated: {{.Header.Timestamp}} + +from __future__ import annotations + +import json +import os +import re +from typing import Any, Optional, Union, TYPE_CHECKING +from urllib.parse import urlparse + +import httpx + +from ._base import APIError, ListOptions + +if TYPE_CHECKING: +{{- range .Spec.Resources}} + from ._{{.Name | snakeCase}}_api import {{.Name}}API +{{- end}} + + +class AmbientClient: + """HTTP client for the Ambient Platform API.""" + + _base_path = "/api/ambient-api-server/v1" + + def __init__( + self, + base_url: str, + token: str, + project: str, + *, + timeout: float = 30.0, + user_agent: str = "ambient-python-sdk/1.0.0", + ) -> None: + self._base_url = base_url.rstrip("/") + self._token = token + self._project = project + self._timeout = timeout + self._user_agent = user_agent + + self._validate_config() + + self._client = httpx.Client( + timeout=timeout, + headers={ + "User-Agent": user_agent, + "Accept": "application/json", + }, + ) + + # Initialize API interfaces +{{- range .Spec.Resources}} + self._{{.Name | snakeCase}}_api: Optional[{{.Name}}API] = None +{{- end}} + + @classmethod + def from_env(cls, **kwargs: Any) -> AmbientClient: + """Create client from environment variables.""" + base_url = os.environ.get("AMBIENT_API_URL", "http://localhost:8080") + token = os.environ.get("AMBIENT_TOKEN") + project = os.environ.get("AMBIENT_PROJECT") + + if not token: + raise ValueError("AMBIENT_TOKEN environment variable is required") + if not project: + raise ValueError("AMBIENT_PROJECT environment variable is required") + + return cls(base_url=base_url, token=token, project=project, **kwargs) + + def _validate_config(self) -> None: + """Validate client configuration.""" + if not self._base_url: + raise ValueError("base URL cannot be empty") + + if "example.com" in self._base_url or "placeholder" in self._base_url: + raise ValueError("placeholder domain is not allowed") + + parsed = urlparse(self._base_url) + if parsed.scheme not in ("http", "https"): + raise ValueError("URL scheme must be http or https") + + if not self._token: + raise ValueError("token cannot be empty") + + if len(self._token) < 20: + raise ValueError("token is too short (minimum 20 characters)") + + if self._token in ("YOUR_TOKEN_HERE", "PLACEHOLDER_TOKEN"): + raise ValueError("placeholder token is not allowed") + + if not self._project: + raise ValueError("project cannot be empty") + + if len(self._project) > 63: + raise ValueError("project name cannot exceed 63 characters") + + if not re.match(r'^[a-z0-9_-]+$', self._project): + raise ValueError("project name must contain only lowercase alphanumeric characters, hyphens, and underscores") + + def _request( + self, + method: str, + path: str, + *, + json: Optional[dict[str, Any]] = None, + params: Optional[dict[str, Any]] = None, + expect_json: bool = True, + ) -> Any: + """Make HTTP request to the API.""" + url = self._base_url + "/api/ambient-api-server/v1" + path + + headers = { + "Authorization": f"Bearer {self._token}", + "X-Ambient-Project": self._project, + } + + if json is not None: + headers["Content-Type"] = "application/json" + + try: + response = self._client.request( + method=method, + url=url, + headers=headers, + json=json, + params=params, + ) + + self._handle_response(response, expect_json) + + if expect_json and response.content: + return response.json() + + return None + + except httpx.RequestError as e: + raise APIError(f"Request failed: {e}") from e + + def _handle_response(self, response: httpx.Response, expect_json: bool) -> None: + """Handle HTTP response, raising appropriate errors.""" + if response.is_success: + return + + try: + if response.content and expect_json: + error_data = response.json() + if isinstance(error_data, dict) and "code" in error_data: + raise APIError.from_dict(error_data, response.status_code) + except (json.JSONDecodeError, ValueError): + pass + + # Fall back to generic error + raise APIError.from_dict( + {"reason": f"HTTP {response.status_code}: {response.reason_phrase}"}, + response.status_code + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._client.close() + + def __enter__(self) -> AmbientClient: + return self + + def __exit__(self, *args: Any) -> None: + self.close() + +{{- range .Spec.Resources}} + @property + def {{.Name | snakeCase | lower | pluralize}}(self) -> {{.Name}}API: + """Get the {{.Name}} API interface.""" + if self._{{.Name | snakeCase}}_api is None: + from ._{{.Name | snakeCase}}_api import {{.Name}}API + self._{{.Name | snakeCase}}_api = {{.Name}}API(self) + return self._{{.Name | snakeCase}}_api +{{- end}} \ No newline at end of file diff --git a/components/ambient-sdk/generator/templates/python/iterator.py.tmpl b/components/ambient-sdk/generator/templates/python/iterator.py.tmpl new file mode 100644 index 000000000..c5c23933d --- /dev/null +++ b/components/ambient-sdk/generator/templates/python/iterator.py.tmpl @@ -0,0 +1,20 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: {{.Header.SpecPath}} +# Spec SHA256: {{.Header.SpecHash}} +# Generated: {{.Header.Timestamp}} + +from __future__ import annotations + +from typing import Any, Callable, Iterator, TypeVar + +T = TypeVar("T") + + +def paginate(fetch_page: Callable[..., Any], size: int = 100) -> Iterator[Any]: + page = 1 + while True: + result = fetch_page(page=page, size=size) + yield from result.items + if page * size >= result.total: + break + page += 1 diff --git a/components/ambient-sdk/generator/templates/python/types.py.tmpl b/components/ambient-sdk/generator/templates/python/types.py.tmpl new file mode 100644 index 000000000..a3254e3b5 --- /dev/null +++ b/components/ambient-sdk/generator/templates/python/types.py.tmpl @@ -0,0 +1,118 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: {{.Header.SpecPath}} +# Spec SHA256: {{.Header.SpecHash}} +# Generated: {{.Header.Timestamp}} + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Optional + +from ._base import ListMeta, _parse_datetime + + +@dataclass(frozen=True) +class {{.Resource.Name}}: + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None +{{- range .Resource.Fields}} + {{.PythonName}}: {{.PythonType}} = {{pythonDefault .}} +{{- end}} + + @classmethod + def from_dict(cls, data: dict) -> {{.Resource.Name}}: + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), +{{- range .Resource.Fields}} +{{- if isDateTime .}} + {{.PythonName}}=_parse_datetime(data.get("{{.Name}}")), +{{- else if eq .PythonType "int"}} + {{.PythonName}}=data.get("{{.Name}}", 0), +{{- else if eq .PythonType "float"}} + {{.PythonName}}=data.get("{{.Name}}", 0.0), +{{- else if eq .PythonType "bool"}} + {{.PythonName}}=data.get("{{.Name}}", False), +{{- else}} + {{.PythonName}}=data.get("{{.Name}}", ""), +{{- end}} +{{- end}} + ) + + @classmethod + def builder(cls) -> {{.Resource.Name}}Builder: + return {{.Resource.Name}}Builder() + + +@dataclass(frozen=True) +class {{.Resource.Name}}List: + kind: str = "" + page: int = 0 + size: int = 0 + total: int = 0 + items: list[{{.Resource.Name}}] = () + + @classmethod + def from_dict(cls, data: dict) -> {{.Resource.Name}}List: + return cls( + kind=data.get("kind", ""), + page=data.get("page", 0), + size=data.get("size", 0), + total=data.get("total", 0), + items=[{{.Resource.Name}}.from_dict(item) for item in data.get("items", [])], + ) + + +class {{.Resource.Name}}Builder: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + +{{range .Resource.Fields}}{{if isWritable .}} + def {{.PythonName}}(self, value: {{.PythonType}}) -> {{$.Resource.Name}}Builder: + self._data["{{.Name}}"] = value + return self +{{end}}{{end}} + def build(self) -> dict: +{{- range .Resource.Fields}} +{{- if .Required}} +{{- if eq .PythonType "str"}} + if "{{.Name}}" not in self._data: + raise ValueError("{{.Name}} is required") +{{- end}} +{{- end}} +{{- end}} + return dict(self._data) + + +class {{.Resource.Name}}Patch: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + +{{range .Resource.PatchFields}} + def {{.PythonName}}(self, value: {{.PythonType}}) -> {{$.Resource.Name}}Patch: + self._data["{{.Name}}"] = value + return self +{{end}} + def to_dict(self) -> dict: + return dict(self._data) +{{if .Resource.HasStatusPatch}} + +class {{.Resource.Name}}StatusPatch: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + +{{range .Resource.StatusPatchFields}} + def {{.PythonName}}(self, value: {{.PythonType}}) -> {{$.Resource.Name}}StatusPatch: + self._data["{{.Name}}"] = value + return self +{{end}} + def to_dict(self) -> dict: + return dict(self._data) +{{end}} diff --git a/components/ambient-sdk/generator/templates/ts/ambient_client.ts.tmpl b/components/ambient-sdk/generator/templates/ts/ambient_client.ts.tmpl new file mode 100644 index 000000000..03c038232 --- /dev/null +++ b/components/ambient-sdk/generator/templates/ts/ambient_client.ts.tmpl @@ -0,0 +1,63 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +import type { AmbientClientConfig } from './base'; +{{range .Spec.Resources}}import { {{.Name}}API } from './{{.Name | snakeCase}}_api'; +{{end}} + +export class AmbientClient { + private readonly config: AmbientClientConfig; +{{range .Spec.Resources}} + readonly {{.Plural | lowerFirst}}: {{.Name}}API; +{{- end}} + + constructor(config: AmbientClientConfig) { + if (!config.baseUrl) { + throw new Error('baseUrl is required'); + } + if (!config.token) { + throw new Error('token is required'); + } + if (config.token.length < 20) { + throw new Error('token is too short (minimum 20 characters)'); + } + if (config.token === 'YOUR_TOKEN_HERE' || config.token === 'PLACEHOLDER_TOKEN') { + throw new Error('placeholder token is not allowed'); + } + if (!config.project) { + throw new Error('project is required'); + } + if (config.project.length > 63) { + throw new Error('project name cannot exceed 63 characters'); + } + + const url = new URL(config.baseUrl); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error('only HTTP and HTTPS schemes are supported'); + } + + this.config = { + ...config, + baseUrl: config.baseUrl.replace(/\/+$/, ''), + }; + +{{range .Spec.Resources}} this.{{.Plural | lowerFirst}} = new {{.Name}}API(this.config); +{{end}} } + + static fromEnv(): AmbientClient { + const baseUrl = process.env.AMBIENT_API_URL || 'http://localhost:8080'; + const token = process.env.AMBIENT_TOKEN; + const project = process.env.AMBIENT_PROJECT; + + if (!token) { + throw new Error('AMBIENT_TOKEN environment variable is required'); + } + if (!project) { + throw new Error('AMBIENT_PROJECT environment variable is required'); + } + + return new AmbientClient({ baseUrl, token, project }); + } +} diff --git a/components/ambient-sdk/generator/templates/ts/base.ts.tmpl b/components/ambient-sdk/generator/templates/ts/base.ts.tmpl new file mode 100644 index 000000000..0e25b23f0 --- /dev/null +++ b/components/ambient-sdk/generator/templates/ts/base.ts.tmpl @@ -0,0 +1,141 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +export type ObjectReference = { + id: string; + kind: string; + href: string; + created_at: string | null; + updated_at: string | null; +}; + +export type ListMeta = { + kind: string; + page: number; + size: number; + total: number; +}; + +export type APIError = { + id: string; + kind: string; + href: string; + code: string; + reason: string; + operation_id: string; + status_code: number; +}; + +export class AmbientAPIError extends Error { + readonly statusCode: number; + readonly code: string; + readonly reason: string; + readonly operationId: string; + + constructor(error: APIError) { + super(`ambient API error ${error.status_code}: ${error.code} — ${error.reason}`); + this.name = 'AmbientAPIError'; + this.statusCode = error.status_code; + this.code = error.code; + this.reason = error.reason; + this.operationId = error.operation_id; + } +} + +export type ListOptions = { + page?: number; + size?: number; + search?: string; + orderBy?: string; + fields?: string; +}; + +export function buildQueryString(opts?: ListOptions): string { + if (!opts) return ''; + const params = new URLSearchParams(); + if (opts.page !== undefined) params.set('page', String(opts.page)); + if (opts.size !== undefined) params.set('size', String(Math.min(opts.size, 65500))); + if (opts.search) params.set('search', opts.search); + if (opts.orderBy) params.set('orderBy', opts.orderBy); + if (opts.fields) params.set('fields', opts.fields); + const qs = params.toString(); + return qs ? `?${qs}` : ''; +} + +export type RequestOptions = { + signal?: AbortSignal; +}; + +export type AmbientClientConfig = { + baseUrl: string; + token: string; + project: string; +}; + +export async function ambientFetch( + config: AmbientClientConfig, + method: string, + path: string, + body?: unknown, + requestOpts?: RequestOptions, +): Promise { + const url = `${config.baseUrl}/api/ambient-api-server/v1${path}`; + const headers: Record = { + 'Authorization': `Bearer ${config.token}`, + 'X-Ambient-Project': config.project, + }; + if (body !== undefined) { + headers['Content-Type'] = 'application/json'; + } + + const resp = await fetch(url, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + signal: requestOpts?.signal, + }); + + if (!resp.ok) { + let errorData: APIError; + try { + const jsonData = await resp.json(); + // Validate that this looks like an APIError response + if (typeof jsonData === 'object' && jsonData !== null) { + errorData = { + id: typeof jsonData.id === 'string' ? jsonData.id : '', + kind: typeof jsonData.kind === 'string' ? jsonData.kind : 'Error', + href: typeof jsonData.href === 'string' ? jsonData.href : '', + code: typeof jsonData.code === 'string' ? jsonData.code : 'unknown_error', + reason: typeof jsonData.reason === 'string' ? jsonData.reason : `HTTP ${resp.status}: ${resp.statusText}`, + operation_id: typeof jsonData.operation_id === 'string' ? jsonData.operation_id : '', + status_code: resp.status, + }; + } else { + throw new Error('Invalid error response format'); + } + } catch { + errorData = { + id: '', + kind: 'Error', + href: '', + code: 'unknown_error', + reason: `HTTP ${resp.status}: ${resp.statusText}`, + operation_id: '', + status_code: resp.status, + }; + } + throw new AmbientAPIError(errorData); + } + + if (resp.status === 204) { + return undefined as T; + } + + // Parse JSON response with validation + const jsonData = await resp.json(); + // Note: In a production system, you might want to add runtime type validation here + // For now, we trust the API contract but avoid the unsafe 'as Promise' cast + return jsonData; +} diff --git a/components/ambient-sdk/generator/templates/ts/client.ts.tmpl b/components/ambient-sdk/generator/templates/ts/client.ts.tmpl new file mode 100644 index 000000000..df68f3334 --- /dev/null +++ b/components/ambient-sdk/generator/templates/ts/client.ts.tmpl @@ -0,0 +1,59 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +import type { AmbientClientConfig, ListOptions, RequestOptions } from './base'; +import { ambientFetch, buildQueryString } from './base'; +import type { {{.Resource.Name}}, {{.Resource.Name}}List, {{.Resource.Name}}CreateRequest{{if .Resource.HasPatch}}, {{.Resource.Name}}PatchRequest{{end}}{{if .Resource.HasStatusPatch}}, {{.Resource.Name}}StatusPatchRequest{{end}} } from './{{.Resource.Name | snakeCase}}'; + +export class {{.Resource.Name}}API { + constructor(private readonly config: AmbientClientConfig) {} + + async create(data: {{.Resource.Name}}CreateRequest, opts?: RequestOptions): Promise<{{.Resource.Name}}> { + return ambientFetch<{{.Resource.Name}}>(this.config, 'POST', '/{{.Resource.PathSegment}}', data, opts); + } + + async get(id: string, opts?: RequestOptions): Promise<{{.Resource.Name}}> { + return ambientFetch<{{.Resource.Name}}>(this.config, 'GET', `/{{.Resource.PathSegment}}/${id}`, undefined, opts); + } + + async list(listOpts?: ListOptions, opts?: RequestOptions): Promise<{{.Resource.Name}}List> { + const qs = buildQueryString(listOpts); + return ambientFetch<{{.Resource.Name}}List>(this.config, 'GET', `/{{.Resource.PathSegment}}${qs}`, undefined, opts); + } + +{{- if .Resource.HasPatch}} + async update(id: string, patch: {{.Resource.Name}}PatchRequest, opts?: RequestOptions): Promise<{{.Resource.Name}}> { + return ambientFetch<{{.Resource.Name}}>(this.config, 'PATCH', `/{{.Resource.PathSegment}}/${id}`, patch, opts); + } +{{end}} +{{- if .Resource.HasDelete}} + async delete(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'DELETE', `/{{.Resource.PathSegment}}/${id}`, undefined, opts); + } +{{end}} +{{- if .Resource.HasStatusPatch}} + async updateStatus(id: string, patch: {{.Resource.Name}}StatusPatchRequest, opts?: RequestOptions): Promise<{{.Resource.Name}}> { + return ambientFetch<{{.Resource.Name}}>(this.config, 'PATCH', `/{{.Resource.PathSegment}}/${id}/status`, patch, opts); + } +{{end}} +{{- range .Resource.Actions}} + async {{.}}(id: string, opts?: RequestOptions): Promise<{{$.Resource.Name}}> { + return ambientFetch<{{$.Resource.Name}}>(this.config, 'POST', `/{{$.Resource.PathSegment}}/${id}/{{.}}`, undefined, opts); + } +{{end}} + async *listAll(size: number = 100, opts?: RequestOptions): AsyncGenerator<{{.Resource.Name}}> { + let page = 1; + while (true) { + const result = await this.list({ page, size }, opts); + for (const item of result.items) { + yield item; + } + if (page * size >= result.total) { + break; + } + page++; + } + } +} diff --git a/components/ambient-sdk/generator/templates/ts/index.ts.tmpl b/components/ambient-sdk/generator/templates/ts/index.ts.tmpl new file mode 100644 index 000000000..ca1d50426 --- /dev/null +++ b/components/ambient-sdk/generator/templates/ts/index.ts.tmpl @@ -0,0 +1,13 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +export { AmbientClient } from './client'; +export type { AmbientClientConfig, ListOptions, RequestOptions, ObjectReference, ListMeta, APIError } from './base'; +export { AmbientAPIError, buildQueryString } from './base'; +{{range .Spec.Resources}} +export type { {{.Name}}, {{.Name}}List, {{.Name}}CreateRequest, {{.Name}}PatchRequest{{if .HasStatusPatch}}, {{.Name}}StatusPatchRequest{{end}} } from './{{.Name | snakeCase}}'; +export { {{.Name}}Builder, {{.Name}}PatchBuilder{{if .HasStatusPatch}}, {{.Name}}StatusPatchBuilder{{end}} } from './{{.Name | snakeCase}}'; +export { {{.Name}}API } from './{{.Name | snakeCase}}_api'; +{{end}} diff --git a/components/ambient-sdk/generator/templates/ts/types.ts.tmpl b/components/ambient-sdk/generator/templates/ts/types.ts.tmpl new file mode 100644 index 000000000..b333d0794 --- /dev/null +++ b/components/ambient-sdk/generator/templates/ts/types.ts.tmpl @@ -0,0 +1,86 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: {{.Header.SpecPath}} +// Spec SHA256: {{.Header.SpecHash}} +// Generated: {{.Header.Timestamp}} + +import type { ObjectReference, ListMeta } from './base'; + +export type {{.Resource.Name}} = ObjectReference & { +{{- range .Resource.Fields}} + {{.Name}}: {{.TSType}}; +{{- end}} +}; + +export type {{.Resource.Name}}List = ListMeta & { + items: {{.Resource.Name}}[]; +}; + +export type {{.Resource.Name}}CreateRequest = { +{{- range .Resource.Fields}}{{if isWritable .}} + {{.Name}}{{if not .Required}}?{{end}}: {{.TSType}}; +{{- end}}{{end}} +}; + +export type {{.Resource.Name}}PatchRequest = { +{{- range .Resource.PatchFields}} + {{.Name}}?: {{.TSType}}; +{{- end}} +}; +{{if .Resource.HasStatusPatch}} +export type {{.Resource.Name}}StatusPatchRequest = { +{{- range .Resource.StatusPatchFields}} + {{.Name}}?: {{.TSType}}; +{{- end}} +}; +{{end}} +export class {{.Resource.Name}}Builder { + private data: Record = {}; + +{{range .Resource.Fields}}{{if isWritable .}} + {{.TSName}}(value: {{.TSType}}): this { + this.data['{{.Name}}'] = value; + return this; + } +{{end}}{{end}} + build(): {{.Resource.Name}}CreateRequest { +{{- range .Resource.Fields}} +{{- if .Required}} +{{- if eq .TSType "string"}} + if (!this.data['{{.Name}}']) { + throw new Error('{{.Name}} is required'); + } +{{- end}} +{{- end}} +{{- end}} + return this.data as {{.Resource.Name}}CreateRequest; + } +} + +export class {{.Resource.Name}}PatchBuilder { + private data: Record = {}; + +{{range .Resource.PatchFields}} + {{.TSName}}(value: {{.TSType}}): this { + this.data['{{.Name}}'] = value; + return this; + } +{{end}} + build(): {{.Resource.Name}}PatchRequest { + return this.data as {{.Resource.Name}}PatchRequest; + } +} +{{if .Resource.HasStatusPatch}} +export class {{.Resource.Name}}StatusPatchBuilder { + private data: Record = {}; + +{{range .Resource.StatusPatchFields}} + {{.TSName}}(value: {{.TSType}}): this { + this.data['{{.Name}}'] = value; + return this; + } +{{end}} + build(): {{.Resource.Name}}StatusPatchRequest { + return this.data as {{.Resource.Name}}StatusPatchRequest; + } +} +{{end}} diff --git a/components/ambient-sdk/go-sdk/README.md b/components/ambient-sdk/go-sdk/README.md new file mode 100644 index 000000000..7fc732d27 --- /dev/null +++ b/components/ambient-sdk/go-sdk/README.md @@ -0,0 +1,393 @@ +# Ambient Platform Go SDK + +Simple HTTP client library for the Ambient Code Platform - Create and manage AI agent sessions without Kubernetes complexity. + +## Installation + +```bash +go get github.com/ambient/platform-sdk +``` + +## Quick Start + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/ambient/platform-sdk/client" + "github.com/ambient/platform-sdk/types" +) + +func main() { + // Create HTTP client + apiURL := "https://your-platform.example.com" + token := os.Getenv("AMBIENT_TOKEN") // Bearer token + project := os.Getenv("AMBIENT_PROJECT") // Project namespace + + client := client.NewClient(apiURL, token, project) + + // Create a session + createReq := &types.CreateSessionRequest{ + Task: "Analyze the repository structure and provide a summary", + Model: "claude-3.5-sonnet", + Repos: []types.RepoHTTP{ + { + URL: "https://github.com/ambient-code/platform", + Branch: "main", + }, + }, + } + + resp, err := client.CreateSession(context.Background(), createReq) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + fmt.Printf("Created session: %s\n", resp.ID) + + // Get session details + session, err := client.GetSession(context.Background(), resp.ID) + if err != nil { + log.Fatalf("Failed to get session: %v", err) + } + + fmt.Printf("Status: %s\n", session.Status) + + // List all sessions + listResp, err := client.ListSessions(context.Background()) + if err != nil { + log.Fatalf("Failed to list sessions: %v", err) + } + + fmt.Printf("Found %d sessions\n", len(listResp.Items)) +} +``` + +## Authentication & Authorization + +The SDK uses Bearer token authentication with project-scoped authorization: + +### Token Requirements + +- **Bearer Token**: Must be a valid authentication token (OpenShift, JWT, or GitHub format) +- **Project Header**: `X-Ambient-Project` specifies the target Kubernetes namespace +- **RBAC**: User must have appropriate permissions in the target namespace + +### Supported Token Formats + +- **OpenShift**: `sha256~...` format tokens from `oc whoami -t` +- **JWT**: Standard JSON Web Tokens with 3 base64 parts +- **GitHub**: Tokens starting with `ghp_`, `gho_`, `ghu_`, or `ghs_` + +### Required Permissions + +Your user account must have these Kubernetes RBAC permissions in the target project/namespace: + +```yaml +# Minimum required permissions +- apiGroups: ["vteam.ambient-code"] + resources: ["agenticsessions"] + verbs: ["get", "list", "create"] + +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get"] +``` + +### Common Permission Errors + +**403 Forbidden**: +```bash +# Check your permissions +oc auth can-i create agenticsessions.vteam.ambient-code -n your-project +oc auth can-i list agenticsessions.vteam.ambient-code -n your-project +``` + +**401 Unauthorized**: +```bash +# Check token validity +oc whoami # Should return your username +oc whoami -t # Should return a token starting with sha256~ +``` + +**400 Bad Request - Project required**: +- Ensure `AMBIENT_PROJECT` environment variable is set +- Project must be a valid Kubernetes namespace name +- User must have access to the specified project + +```bash +# Set environment variables +export AMBIENT_TOKEN="your-bearer-token" # Required +export AMBIENT_PROJECT="your-project-name" # Required +export AMBIENT_API_URL="https://your-api.com" # Optional +``` + +**OpenShift Users:** +```bash +# Use your OpenShift token +export AMBIENT_TOKEN="$(oc whoami -t)" +export AMBIENT_PROJECT="$(oc project -q)" +``` + +## Core Operations + +### Create Session + +```go +createReq := &types.CreateSessionRequest{ + Task: "Review this code for security issues", + Model: "claude-3.5-sonnet", // Optional, uses platform default if omitted + Repos: []types.RepoHTTP{ + {URL: "https://github.com/user/repo", Branch: "main"}, + }, +} + +resp, err := client.CreateSession(ctx, createReq) +``` + +### Get Session Details + +```go +session, err := client.GetSession(ctx, "session-1234567") +if err != nil { + log.Printf("Session error: %v", err) +} + +fmt.Printf("Status: %s\n", session.Status) +if session.Status == types.StatusCompleted { + fmt.Printf("Result: %s\n", session.Result) +} +``` + +### List Sessions + +```go +listResp, err := client.ListSessions(ctx) +if err != nil { + return err +} + +for _, session := range listResp.Items { + fmt.Printf("- %s (%s): %s\n", session.ID, session.Status, session.Task) +} +``` + +### Monitor Session Completion + +```go +// Wait for session to complete +completed, err := client.WaitForCompletion(ctx, sessionID, 5*time.Second) +if err != nil { + return fmt.Errorf("monitoring failed: %w", err) +} + +if completed.Status == types.StatusCompleted { + fmt.Printf("Success: %s\n", completed.Result) +} else { + fmt.Printf("Failed: %s\n", completed.Error) +} +``` + +## Session Status Values + +```go +const ( + StatusPending = "pending" // Session created, waiting to start + StatusRunning = "running" // AI agent actively working + StatusCompleted = "completed" // Task finished successfully + StatusFailed = "failed" // Task failed with error +) +``` + +## Configuration Options + +### Custom Timeout + +```go +client := client.NewClientWithTimeout(apiURL, token, project, 60*time.Second) +``` + +### Error Handling + +```go +session, err := client.GetSession(ctx, sessionID) +if err != nil { + // Detailed error messages include HTTP status and API responses + log.Printf("Failed: %v", err) + // Example: "API error (404): session not found: session-xyz" +} +``` + +## Examples + +See the `examples/` directory for complete working examples: + +- **`main.go`** - Complete session lifecycle demonstration +- **`README.md`** - Detailed usage guide with troubleshooting + +## API Reference + +### Client Methods + +```go +// Client creation +func NewClient(baseURL, token, project string) *Client +func NewClientWithTimeout(baseURL, token, project string, timeout time.Duration) *Client + +// Session operations +func (c *Client) CreateSession(ctx context.Context, req *CreateSessionRequest) (*CreateSessionResponse, error) +func (c *Client) GetSession(ctx context.Context, sessionID string) (*SessionResponse, error) +func (c *Client) ListSessions(ctx context.Context) (*SessionListResponse, error) +func (c *Client) WaitForCompletion(ctx context.Context, sessionID string, pollInterval time.Duration) (*SessionResponse, error) +``` + +### Types + +```go +// Request types +type CreateSessionRequest struct { + Task string `json:"task"` + Model string `json:"model,omitempty"` + Repos []RepoHTTP `json:"repos,omitempty"` +} + +type RepoHTTP struct { + URL string `json:"url"` + Branch string `json:"branch,omitempty"` +} + +// Response types +type SessionResponse struct { + ID string `json:"id"` + Status string `json:"status"` + Task string `json:"task"` + Model string `json:"model,omitempty"` + CreatedAt string `json:"createdAt"` + CompletedAt string `json:"completedAt,omitempty"` + Result string `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +type SessionListResponse struct { + Items []SessionResponse `json:"items"` + Total int `json:"total"` +} + +type CreateSessionResponse struct { + ID string `json:"id"` + Message string `json:"message"` +} + +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message,omitempty"` +} +``` + +## Architecture + +### Design Principles + +- **HTTP-First**: Pure REST API client with no Kubernetes dependencies +- **Minimal Dependencies**: Uses only Go standard library +- **Simple Integration**: Easy to embed in any Go application +- **Type Safety**: Strongly-typed requests and responses with compile-time validation +- **Clear Separation**: Public SDK vs internal platform implementation + +### HTTP vs Kubernetes + +This SDK provides a **simplified HTTP interface** to the Ambient Platform: + +| Aspect | HTTP SDK (This Package) | Internal Platform | +|--------|------------------------|-------------------| +| **API** | Simple REST endpoints (`/v1/sessions`) | Complex Kubernetes CRDs | +| **Auth** | Bearer token + project header | RBAC + service accounts | +| **Types** | Flat JSON structs | Full K8s metadata/spec/status | +| **Usage** | Any HTTP client, any environment | Kubernetes cluster access required | +| **Target** | External integrators, simple automation | Internal platform components | + +### Internal vs Public + +- **Backend Components**: Can use internal Kubernetes types for cluster operations +- **SDK Users**: Get simplified HTTP API without Kubernetes complexity +- **Type Definitions**: Shared between internal and public usage where appropriate + +## Migration from Kubernetes SDK + +If migrating from a previous Kubernetes-based version: + +### Before (Kubernetes) +```go +import "k8s.io/client-go/kubernetes" + +client, err := sdk.NewClientFromKubeconfig("") +session := &types.AgenticSession{/* complex K8s structure */} +created, err := client.Sessions.Create(ctx, session) +``` + +### After (HTTP) +```go +import "github.com/ambient/platform-sdk/client" + +client := client.NewClient(apiURL, token, project) +req := &types.CreateSessionRequest{Task: "...", Model: "..."} +resp, err := client.CreateSession(ctx, req) +``` + +## Troubleshooting + +### Authentication Issues +``` +❌ AMBIENT_TOKEN environment variable is required +``` +**Solution**: Set your Bearer token: `export AMBIENT_TOKEN="your-token"` + +### Project Header Missing +``` +API error (400): Project required. Set X-Ambient-Project header +``` +**Solution**: Set project name: `export AMBIENT_PROJECT="your-project"` + +### Connection Errors +``` +Failed to execute request: dial tcp: connection refused +``` +**Solution**: Verify API endpoint and network connectivity + +### Session Not Found +``` +API error (404): session not found: session-xyz +``` +**Solution**: Verify session ID and check if you have access to the project + +## Testing + +```bash +go test ./... +``` + +Run the complete example: +```bash +cd examples/ +export AMBIENT_TOKEN="your-token" +export AMBIENT_PROJECT="your-project" +go run main.go +``` + +## OpenAPI Specification + +This SDK is built to match the canonical OpenAPI specification owned by the API server at `../../ambient-api-server/openapi/openapi.yaml`. The SDK does not maintain its own spec copy — types and client behavior derive from the API server's definitions. + +## Contributing + +1. **SDK Changes**: Modify code in `client/` or `types/` directories +2. **API Changes**: Update `../openapi.yaml` specification first +3. **Examples**: Add working examples to `examples/` directory +4. **Testing**: Ensure all changes work with real API endpoints + +For complete platform documentation, see the main [platform repository](https://github.com/ambient-code/platform). \ No newline at end of file diff --git a/components/ambient-sdk/go-sdk/client/client.go b/components/ambient-sdk/go-sdk/client/client.go new file mode 100644 index 000000000..2c2d2875d --- /dev/null +++ b/components/ambient-sdk/go-sdk/client/client.go @@ -0,0 +1,232 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "time" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +type Client struct { + httpClient *http.Client + baseURL string + token string + project string + logger *slog.Logger + userAgent string +} + +type ClientOption func(*Client) + +func WithTimeout(timeout time.Duration) ClientOption { + return func(c *Client) { + c.httpClient.Timeout = timeout + } +} + +func WithLogger(logger *slog.Logger) ClientOption { + return func(c *Client) { + c.logger = logger + } +} + +func WithUserAgent(userAgent string) ClientOption { + return func(c *Client) { + c.userAgent = userAgent + } +} + +func NewClient(baseURL, token, project string, opts ...ClientOption) (*Client, error) { + if token == "" { + return nil, fmt.Errorf("token is required") + } + + if len(token) < 20 { + return nil, fmt.Errorf("token is too short (minimum 20 characters)") + } + + if token == "YOUR_TOKEN_HERE" || token == "PLACEHOLDER_TOKEN" { + return nil, fmt.Errorf("placeholder token is not allowed") + } + + if project == "" { + return nil, fmt.Errorf("project is required") + } + + if len(project) > 63 { + return nil, fmt.Errorf("project name cannot exceed 63 characters") + } + + if err := validateURL(baseURL); err != nil { + return nil, fmt.Errorf("invalid base URL: %w", err) + } + + c := &Client{ + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + baseURL: strings.TrimSuffix(baseURL, "/"), + token: token, + project: project, + logger: slog.Default(), + userAgent: "ambient-go-sdk/1.0.0", + } + + for _, opt := range opts { + opt(c) + } + + c.logger = c.logger.With(slog.String("sdk", "go"), slog.String("project", project)) + + return c, nil +} + +func NewClientFromEnv(opts ...ClientOption) (*Client, error) { + baseURL := os.Getenv("AMBIENT_API_URL") + if baseURL == "" { + baseURL = "http://localhost:8080" + } + + token := os.Getenv("AMBIENT_TOKEN") + if token == "" { + return nil, fmt.Errorf("AMBIENT_TOKEN environment variable is required") + } + + project := os.Getenv("AMBIENT_PROJECT") + if project == "" { + return nil, fmt.Errorf("AMBIENT_PROJECT environment variable is required") + } + + return NewClient(baseURL, token, project, opts...) +} + +func (c *Client) do(ctx context.Context, method, path string, body []byte, expectedStatus int, result interface{}) error { + url := c.baseURL + "/api/ambient-api-server/v1" + path + + req, err := http.NewRequestWithContext(ctx, method, url, nil) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + + if body != nil { + req.Body = io.NopCloser(bytes.NewReader(body)) + req.ContentLength = int64(len(body)) + req.Header.Set("Content-Type", "application/json") + } + + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("X-Ambient-Project", c.project) + req.Header.Set("User-Agent", c.userAgent) + req.Header.Set("Accept", "application/json") + + c.logger.Debug("HTTP request", + slog.String("method", method), + slog.String("url", sanitizeLogURL(url)), + slog.Int("body_len", len(body)), + ) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response body: %w", err) + } + + c.logger.Debug("HTTP response", + slog.Int("status", resp.StatusCode), + slog.Int("body_len", len(respBody)), + ) + + if resp.StatusCode != expectedStatus { + var apiErr types.APIError + if json.Unmarshal(respBody, &apiErr) == nil && apiErr.Code != "" { + apiErr.StatusCode = resp.StatusCode + return &apiErr + } + return &types.APIError{ + StatusCode: resp.StatusCode, + Code: "http_error", + Reason: fmt.Sprintf("HTTP %d: unexpected status", resp.StatusCode), + } + } + + if result != nil && len(respBody) > 0 { + if err := json.Unmarshal(respBody, result); err != nil { + return fmt.Errorf("unmarshal response: %w", err) + } + } + + return nil +} + +func (c *Client) doWithQuery(ctx context.Context, method, path string, body []byte, expectedStatus int, result interface{}, opts *types.ListOptions) error { + queryPath := path + if opts != nil { + params := url.Values{} + if opts.Page > 0 { + params.Set("page", fmt.Sprintf("%d", opts.Page)) + } + if opts.Size > 0 { + params.Set("size", fmt.Sprintf("%d", opts.Size)) + } + if opts.Search != "" { + params.Set("search", opts.Search) + } + if opts.OrderBy != "" { + params.Set("orderBy", opts.OrderBy) + } + if opts.Fields != "" { + params.Set("fields", opts.Fields) + } + if len(params) > 0 { + queryPath += "?" + params.Encode() + } + } + + return c.do(ctx, method, queryPath, body, expectedStatus, result) +} + +func validateURL(rawURL string) error { + if rawURL == "" { + return fmt.Errorf("URL cannot be empty") + } + + if strings.Contains(rawURL, "example.com") || strings.Contains(rawURL, "placeholder") { + return fmt.Errorf("placeholder URLs are not allowed") + } + + u, err := url.Parse(rawURL) + if err != nil { + return fmt.Errorf("invalid URL format: %w", err) + } + + if u.Scheme != "http" && u.Scheme != "https" { + return fmt.Errorf("only HTTP and HTTPS schemes are supported") + } + + return nil +} + +func sanitizeLogURL(rawURL string) string { + tokenPattern := regexp.MustCompile(`([Bb]earer\s+)[a-zA-Z0-9\-_~.+/=]+`) + return tokenPattern.ReplaceAllString(rawURL, "${1}[REDACTED]") +} diff --git a/components/ambient-sdk/go-sdk/client/iterator.go b/components/ambient-sdk/go-sdk/client/iterator.go new file mode 100644 index 000000000..2a8b05049 --- /dev/null +++ b/components/ambient-sdk/go-sdk/client/iterator.go @@ -0,0 +1,65 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package client + +type Listable[T any] interface { + GetItems() []T + GetTotal() int + GetPage() int + GetSize() int +} + +type Iterator[T any] struct { + fetchPage func(page int) (Listable[T], error) + items []T + index int + page int + total int + done bool + err error +} + +func NewIterator[T any, L Listable[T]](fetchPage func(page int) (L, error)) *Iterator[T] { + return &Iterator[T]{ + fetchPage: func(page int) (Listable[T], error) { + return fetchPage(page) + }, + page: 0, + index: -1, + } +} + +func (it *Iterator[T]) Next() bool { + if it.done || it.err != nil { + return false + } + it.index++ + if it.index < len(it.items) { + return true + } + it.page++ + result, err := it.fetchPage(it.page) + if err != nil { + it.err = err + return false + } + it.items = result.GetItems() + it.index = 0 + it.total = result.GetTotal() + if len(it.items) == 0 { + it.done = true + return false + } + return true +} + +func (it *Iterator[T]) Item() T { + return it.items[it.index] +} + +func (it *Iterator[T]) Err() error { + return it.err +} diff --git a/components/ambient-sdk/go-sdk/client/project_api.go b/components/ambient-sdk/go-sdk/client/project_api.go new file mode 100644 index 000000000..c88f2224b --- /dev/null +++ b/components/ambient-sdk/go-sdk/client/project_api.go @@ -0,0 +1,75 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +type ProjectAPI struct { + client *Client +} + +func (c *Client) Projects() *ProjectAPI { + return &ProjectAPI{client: c} +} + +func (a *ProjectAPI) Create(ctx context.Context, resource *types.Project) (*types.Project, error) { + body, err := json.Marshal(resource) + if err != nil { + return nil, fmt.Errorf("marshal project: %w", err) + } + var result types.Project + if err := a.client.do(ctx, http.MethodPost, "/projects", body, http.StatusCreated, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ProjectAPI) Get(ctx context.Context, id string) (*types.Project, error) { + var result types.Project + if err := a.client.do(ctx, http.MethodGet, "/projects/"+url.PathEscape(id), nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ProjectAPI) List(ctx context.Context, opts *types.ListOptions) (*types.ProjectList, error) { + var result types.ProjectList + if err := a.client.doWithQuery(ctx, http.MethodGet, "/projects", nil, http.StatusOK, &result, opts); err != nil { + return nil, err + } + return &result, nil +} +func (a *ProjectAPI) Update(ctx context.Context, id string, patch map[string]any) (*types.Project, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal patch: %w", err) + } + var result types.Project + if err := a.client.do(ctx, http.MethodPatch, "/projects/"+url.PathEscape(id), body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ProjectAPI) Delete(ctx context.Context, id string) error { + return a.client.do(ctx, http.MethodDelete, "/projects/"+url.PathEscape(id), nil, http.StatusNoContent, nil) +} + +func (a *ProjectAPI) ListAll(ctx context.Context, opts *types.ListOptions) *Iterator[types.Project] { + return NewIterator(func(page int) (*types.ProjectList, error) { + o := *opts + o.Page = page + return a.List(ctx, &o) + }) +} diff --git a/components/ambient-sdk/go-sdk/client/project_settings_api.go b/components/ambient-sdk/go-sdk/client/project_settings_api.go new file mode 100644 index 000000000..fcd7a3bd3 --- /dev/null +++ b/components/ambient-sdk/go-sdk/client/project_settings_api.go @@ -0,0 +1,75 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +type ProjectSettingsAPI struct { + client *Client +} + +func (c *Client) ProjectSettings() *ProjectSettingsAPI { + return &ProjectSettingsAPI{client: c} +} + +func (a *ProjectSettingsAPI) Create(ctx context.Context, resource *types.ProjectSettings) (*types.ProjectSettings, error) { + body, err := json.Marshal(resource) + if err != nil { + return nil, fmt.Errorf("marshal project_settings: %w", err) + } + var result types.ProjectSettings + if err := a.client.do(ctx, http.MethodPost, "/project_settings", body, http.StatusCreated, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ProjectSettingsAPI) Get(ctx context.Context, id string) (*types.ProjectSettings, error) { + var result types.ProjectSettings + if err := a.client.do(ctx, http.MethodGet, "/project_settings/"+url.PathEscape(id), nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ProjectSettingsAPI) List(ctx context.Context, opts *types.ListOptions) (*types.ProjectSettingsList, error) { + var result types.ProjectSettingsList + if err := a.client.doWithQuery(ctx, http.MethodGet, "/project_settings", nil, http.StatusOK, &result, opts); err != nil { + return nil, err + } + return &result, nil +} +func (a *ProjectSettingsAPI) Update(ctx context.Context, id string, patch map[string]any) (*types.ProjectSettings, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal patch: %w", err) + } + var result types.ProjectSettings + if err := a.client.do(ctx, http.MethodPatch, "/project_settings/"+url.PathEscape(id), body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ProjectSettingsAPI) Delete(ctx context.Context, id string) error { + return a.client.do(ctx, http.MethodDelete, "/project_settings/"+url.PathEscape(id), nil, http.StatusNoContent, nil) +} + +func (a *ProjectSettingsAPI) ListAll(ctx context.Context, opts *types.ListOptions) *Iterator[types.ProjectSettings] { + return NewIterator(func(page int) (*types.ProjectSettingsList, error) { + o := *opts + o.Page = page + return a.List(ctx, &o) + }) +} diff --git a/components/ambient-sdk/go-sdk/client/session_api.go b/components/ambient-sdk/go-sdk/client/session_api.go new file mode 100644 index 000000000..8aa5f83b4 --- /dev/null +++ b/components/ambient-sdk/go-sdk/client/session_api.go @@ -0,0 +1,99 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +type SessionAPI struct { + client *Client +} + +func (c *Client) Sessions() *SessionAPI { + return &SessionAPI{client: c} +} + +func (a *SessionAPI) Create(ctx context.Context, resource *types.Session) (*types.Session, error) { + body, err := json.Marshal(resource) + if err != nil { + return nil, fmt.Errorf("marshal session: %w", err) + } + var result types.Session + if err := a.client.do(ctx, http.MethodPost, "/sessions", body, http.StatusCreated, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SessionAPI) Get(ctx context.Context, id string) (*types.Session, error) { + var result types.Session + if err := a.client.do(ctx, http.MethodGet, "/sessions/"+url.PathEscape(id), nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SessionAPI) List(ctx context.Context, opts *types.ListOptions) (*types.SessionList, error) { + var result types.SessionList + if err := a.client.doWithQuery(ctx, http.MethodGet, "/sessions", nil, http.StatusOK, &result, opts); err != nil { + return nil, err + } + return &result, nil +} +func (a *SessionAPI) Update(ctx context.Context, id string, patch map[string]any) (*types.Session, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal patch: %w", err) + } + var result types.Session + if err := a.client.do(ctx, http.MethodPatch, "/sessions/"+url.PathEscape(id), body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SessionAPI) UpdateStatus(ctx context.Context, id string, patch map[string]any) (*types.Session, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal status patch: %w", err) + } + var result types.Session + if err := a.client.do(ctx, http.MethodPatch, "/sessions/"+url.PathEscape(id)+"/status", body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SessionAPI) Start(ctx context.Context, id string) (*types.Session, error) { + var result types.Session + if err := a.client.do(ctx, http.MethodPost, "/sessions/"+url.PathEscape(id)+"/start", nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SessionAPI) Stop(ctx context.Context, id string) (*types.Session, error) { + var result types.Session + if err := a.client.do(ctx, http.MethodPost, "/sessions/"+url.PathEscape(id)+"/stop", nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SessionAPI) ListAll(ctx context.Context, opts *types.ListOptions) *Iterator[types.Session] { + return NewIterator(func(page int) (*types.SessionList, error) { + o := *opts + o.Page = page + return a.List(ctx, &o) + }) +} diff --git a/components/ambient-sdk/go-sdk/client/user_api.go b/components/ambient-sdk/go-sdk/client/user_api.go new file mode 100644 index 000000000..e313ad087 --- /dev/null +++ b/components/ambient-sdk/go-sdk/client/user_api.go @@ -0,0 +1,71 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +type UserAPI struct { + client *Client +} + +func (c *Client) Users() *UserAPI { + return &UserAPI{client: c} +} + +func (a *UserAPI) Create(ctx context.Context, resource *types.User) (*types.User, error) { + body, err := json.Marshal(resource) + if err != nil { + return nil, fmt.Errorf("marshal user: %w", err) + } + var result types.User + if err := a.client.do(ctx, http.MethodPost, "/users", body, http.StatusCreated, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *UserAPI) Get(ctx context.Context, id string) (*types.User, error) { + var result types.User + if err := a.client.do(ctx, http.MethodGet, "/users/"+url.PathEscape(id), nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *UserAPI) List(ctx context.Context, opts *types.ListOptions) (*types.UserList, error) { + var result types.UserList + if err := a.client.doWithQuery(ctx, http.MethodGet, "/users", nil, http.StatusOK, &result, opts); err != nil { + return nil, err + } + return &result, nil +} +func (a *UserAPI) Update(ctx context.Context, id string, patch map[string]any) (*types.User, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal patch: %w", err) + } + var result types.User + if err := a.client.do(ctx, http.MethodPatch, "/users/"+url.PathEscape(id), body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *UserAPI) ListAll(ctx context.Context, opts *types.ListOptions) *Iterator[types.User] { + return NewIterator(func(page int) (*types.UserList, error) { + o := *opts + o.Page = page + return a.List(ctx, &o) + }) +} diff --git a/components/ambient-sdk/go-sdk/examples/README.md b/components/ambient-sdk/go-sdk/examples/README.md new file mode 100644 index 000000000..26b37ac52 --- /dev/null +++ b/components/ambient-sdk/go-sdk/examples/README.md @@ -0,0 +1,255 @@ +# Ambient Platform Go SDK Examples + +This directory contains examples demonstrating how to use the Ambient Platform Go SDK to interact with the platform via HTTP API. + +## Overview + +The Ambient Platform SDK provides a simple HTTP client for creating and managing AI agent sessions through the platform's public REST API. No Kubernetes dependencies are required - just HTTP and Bearer token authentication. + +## Quick Start + +### Prerequisites + +1. **Ambient Platform**: Deployed and accessible via HTTP +2. **Bearer Token**: Valid authentication token for API access +3. **Go 1.21+**: For running the examples + +### Environment Setup + +Set the required environment variables: + +```bash +# Required: Your API authentication token +export AMBIENT_TOKEN="your-bearer-token-here" + +# Required: Project name (Kubernetes namespace) +export AMBIENT_PROJECT="your-project-name" + +# Optional: API endpoint (defaults to working public API URL) +export AMBIENT_API_URL="https://your-platform.example.com" + +# Optional: Monitor session completion (defaults to false) +export MONITOR_SESSION="true" +``` + +### Running the Examples + +```bash +# Run the HTTP client example (recommended) +go run main.go + +# Legacy Kubernetes example (deprecated - requires k8s access) +go run kubernetes_main.go +``` + +The HTTP example demonstrates: +- ✅ Creating a new agentic session +- ✅ Retrieving session details by ID +- ✅ Listing all accessible sessions +- ✅ Optional session completion monitoring + +## Example Output + +``` +🌐 Ambient Platform SDK - HTTP Client Example +============================================ +✓ Created client for API: https://public-api-route-mturansk.apps.rosa.xezue-pjejw-oy9.ag90.p3.openshiftapps.com +✓ Using project: mturansk + +📝 Creating new session... +✓ Created session: session-1771013589 + +🔍 Getting session details... + ID: session-1771013589 + Status: pending + Task: Analyze the repository structure and provide a brief summary... + Model: claude-3.5-sonnet + Created: 2026-02-13T20:13:09Z + +📋 Listing all sessions... +✓ Found 2 sessions (total: 2) + 1. session-1771013589 (pending) - Analyze the repository structure and... + 2. example-session (pending) - + +✅ HTTP Client demonstration complete! +``` + +## Key Features + +### Simple HTTP Client +- Pure REST API integration with no Kubernetes dependencies +- Bearer token authentication with project routing +- JSON request/response handling +- Proper error handling with detailed messages + +### Session Management +- **Create**: Submit new tasks to AI agents +- **Retrieve**: Get session details and status +- **List**: Browse all accessible sessions +- **Monitor**: Poll for session completion + +### Type Safety +- Strongly-typed request and response structures +- Compile-time validation of API payloads +- Clear error messages for debugging + +## API Reference + +### Client Creation + +```go +import "github.com/ambient/platform-sdk/client" + +// Basic client +client := client.NewClient(apiURL, token, project) + +// Client with custom timeout +client := client.NewClientWithTimeout(apiURL, token, project, 60*time.Second) +``` + +### Session Operations + +```go +// Create session +createReq := &types.CreateSessionRequest{ + Task: "Analyze this repository", + Model: "claude-3.5-sonnet", + Repos: []types.RepoHTTP{{URL: "https://github.com/user/repo"}}, +} +resp, err := client.CreateSession(ctx, createReq) + +// Get session +session, err := client.GetSession(ctx, sessionID) + +// List sessions +list, err := client.ListSessions(ctx) + +// Wait for completion +completed, err := client.WaitForCompletion(ctx, sessionID, 5*time.Second) +``` + +## Working Configuration (Tested) + +The following configuration has been tested and verified working: + +```bash +# Tested working configuration +export AMBIENT_API_URL="https://public-api-route-mturansk.apps.rosa.xezue-pjejw-oy9.ag90.p3.openshiftapps.com" +export AMBIENT_TOKEN="$(oc whoami -t)" # OpenShift token +export AMBIENT_PROJECT="mturansk" # Valid namespace +export MONITOR_SESSION="true" # Enable completion monitoring + +go run main.go +``` + +## Error Handling + +The SDK provides detailed error information: + +```go +session, err := client.GetSession(ctx, "invalid-id") +if err != nil { + // Errors include HTTP status codes and API error messages + log.Printf("Failed to get session: %v", err) + // Example: "API error (404): session not found: invalid-id" +} +``` + +## Troubleshooting + +### Authentication Issues +``` +❌ AMBIENT_TOKEN environment variable is required +``` +**Solution**: Set your Bearer token in the `AMBIENT_TOKEN` environment variable. + +### Project Header Missing +``` +API error (400): Project required. Set X-Ambient-Project header... +``` +**Solution**: Set the `AMBIENT_PROJECT` environment variable to a valid namespace. + +### Connection Errors +``` +Failed to execute request: dial tcp: connection refused +``` +**Solution**: Verify the API endpoint is correct and accessible. Check `AMBIENT_API_URL`. + +### API Errors +``` +API error (401): Unauthorized - Invalid or expired token +``` +**Solution**: Verify your Bearer token is valid and has appropriate permissions. + +### Session Not Found +``` +API error (404): session not found: session-xyz +``` +**Solution**: Verify the session ID exists and you have access to it. + +## Migration from Legacy Example + +If you're migrating from the previous Kubernetes-based example: + +### Old (Kubernetes) +```go +import "k8s.io/client-go/kubernetes" + +client, err := client.NewClientFromKubeconfig("") +session, err := client.Sessions.Create(ctx, agenticSession) +``` + +### New (HTTP) +```go +import "github.com/ambient/platform-sdk/client" + +client := client.NewClient(apiURL, token, project) +resp, err := client.CreateSession(ctx, sessionRequest) +``` + +## Architecture Notes + +### Design Principles +- **HTTP-First**: Pure REST API client with no Kubernetes dependencies +- **Minimal Dependencies**: Uses only Go standard library +- **Simple Integration**: Easy to embed in any Go application +- **Clear Separation**: Public SDK vs internal platform implementation + +### Internal vs Public +- **Backend**: Can continue using strongly-typed Kubernetes structs for internal operations +- **SDK**: Exposes only HTTP API functionality for external integrators +- **Types**: Shared type definitions support both internal and public usage + +## Session Monitoring + +The SDK supports three approaches to session completion monitoring: + +1. **Simple Polling** (Implemented): + ```go + session, err := client.WaitForCompletion(ctx, sessionID, 5*time.Second) + ``` + +2. **Manual Status Checking**: + ```go + for { + session, err := client.GetSession(ctx, sessionID) + if session.Status == types.StatusCompleted || session.Status == types.StatusFailed { + break + } + time.Sleep(5 * time.Second) + } + ``` + +3. **Future: WebSocket/SSE** (Planned): + - Real-time status updates + - Progress streaming + - Event notifications + +## Next Steps + +1. **Explore Advanced Features**: Session monitoring, batch operations +2. **Integration Testing**: Test with your actual platform deployment +3. **Error Handling**: Implement retry logic and circuit breakers +4. **Observability**: Add logging and metrics for production usage + +For complete API documentation, see the [public API reference](../../public-api/README.md). \ No newline at end of file diff --git a/components/ambient-sdk/go-sdk/examples/go.mod b/components/ambient-sdk/go-sdk/examples/go.mod new file mode 100644 index 000000000..bd0838b47 --- /dev/null +++ b/components/ambient-sdk/go-sdk/examples/go.mod @@ -0,0 +1,7 @@ +module github.com/ambient-code/platform/components/ambient-sdk/go-sdk/examples + +go 1.21 + +replace github.com/ambient-code/platform/components/ambient-sdk/go-sdk => ../ + +require github.com/ambient-code/platform/components/ambient-sdk/go-sdk v0.0.0-00010101000000-000000000000 diff --git a/components/ambient-sdk/go-sdk/examples/go.sum b/components/ambient-sdk/go-sdk/examples/go.sum new file mode 100644 index 000000000..e69de29bb diff --git a/components/ambient-sdk/go-sdk/examples/main.go b/components/ambient-sdk/go-sdk/examples/main.go new file mode 100644 index 000000000..b1c858397 --- /dev/null +++ b/components/ambient-sdk/go-sdk/examples/main.go @@ -0,0 +1,367 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/client" + "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/types" +) + +func main() { + fmt.Println("Ambient Platform SDK — End-to-End Example") + fmt.Println("==========================================") + fmt.Println() + + c, err := client.NewClientFromEnv(client.WithTimeout(120 * time.Second)) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + ctx := context.Background() + + projectName := os.Getenv("AMBIENT_PROJECT") + if projectName == "" { + projectName = os.Getenv("ANTHROPIC_VERTEX_PROJECT_ID") + } + if projectName == "" { + projectName = "sdk-demo" + } + + runFullLifecycle(ctx, c, projectName) +} + +func runFullLifecycle(ctx context.Context, c *client.Client, projectName string) { + fmt.Println("Step 1: Create Workspace (Project)") + fmt.Println("-----------------------------------") + + project, err := types.NewProjectBuilder(). + Name(projectName). + DisplayName("SDK Demo Workspace"). + Description("Created programmatically via Go SDK"). + Build() + if err != nil { + log.Fatalf("Failed to build project: %v", err) + } + + createdProject, err := c.Projects().Create(ctx, project) + if err != nil { + var apiErr *types.APIError + if ok := asAPIError(err, &apiErr); ok && apiErr.StatusCode == http.StatusConflict { + fmt.Printf(" Project %q already exists, reusing it\n", projectName) + createdProject, err = c.Projects().Get(ctx, projectName) + if err != nil { + log.Fatalf("Failed to get existing project: %v", err) + } + } else { + log.Fatalf("Failed to create project: %v", err) + } + } else { + fmt.Printf(" Created project: %s (id=%s)\n", createdProject.Name, createdProject.ID) + } + fmt.Println() + + fmt.Println("Step 2: Create Session with Repository Context") + fmt.Println("-----------------------------------------------") + + reposJSON, _ := json.Marshal([]map[string]interface{}{ + { + "url": "https://github.com/ambient-code/platform", + "branch": "main", + "autoPush": false, + }, + }) + + session, err := types.NewSessionBuilder(). + Name("sdk-demo-session"). + Prompt("Analyze the repository structure and summarize the key components"). + Timeout(300). + LlmModel("claude-sonnet-4-20250514"). + LlmTemperature(0.7). + LlmMaxTokens(4096). + Repos(string(reposJSON)). + Build() + if err != nil { + log.Fatalf("Failed to build session: %v", err) + } + + createdSession, err := c.Sessions().Create(ctx, session) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + fmt.Printf(" Created session: %s\n", createdSession.Name) + fmt.Printf(" Session ID: %s\n", createdSession.ID) + fmt.Printf(" Phase: %s\n", phaseOrDefault(createdSession.Phase)) + fmt.Printf(" Model: %s\n", createdSession.LlmModel) + fmt.Printf(" Repos: %s\n", createdSession.Repos) + fmt.Println() + + fmt.Println("Step 3: Start Session") + fmt.Println("---------------------") + + startedSession, err := c.Sessions().Start(ctx, createdSession.ID) + if err != nil { + log.Fatalf("Failed to start session: %v", err) + } + fmt.Printf(" Start requested. Phase: %s\n", phaseOrDefault(startedSession.Phase)) + fmt.Println() + + fmt.Println("Step 4: Wait for Session to Reach Running Phase") + fmt.Println("------------------------------------------------") + + runningSession, err := waitForPhase(ctx, c, createdSession.ID, "Running", 5*time.Minute) + if err != nil { + fmt.Printf(" Warning: %v\n", err) + fmt.Println(" (The session may still be starting — the operator creates a runner pod)") + fmt.Println(" Continuing with demonstration...") + } else { + fmt.Printf(" Session is Running! Phase: %s\n", runningSession.Phase) + if runningSession.StartTime != nil { + fmt.Printf(" Started at: %s\n", runningSession.StartTime.Format(time.RFC3339)) + } + } + fmt.Println() + + fmt.Println("Step 5: Send a Message via AG-UI") + fmt.Println("--------------------------------") + + apiURL := os.Getenv("AMBIENT_API_URL") + token := os.Getenv("AMBIENT_TOKEN") + + aguiBaseURL := deriveAGUIBaseURL(apiURL, projectName, createdSession.KubeCrName, createdSession.ID) + + fmt.Printf(" AG-UI endpoint: %s\n", aguiBaseURL) + fmt.Println(" Sending user message...") + + reply, err := sendMessageAndWaitForReply(ctx, aguiBaseURL, token, createdSession.ID, "Describe the backend components in this repository") + if err != nil { + fmt.Printf(" AG-UI messaging not available: %v\n", err) + fmt.Println(" (This requires the full stack: operator + runner pod running in the cluster)") + fmt.Println(" The session is created and visible in the UI — you can chat with it there.") + } else { + fmt.Println(" Assistant reply (first 500 chars):") + fmt.Println(" " + truncate(reply, 500)) + } + fmt.Println() + + fmt.Println("Step 6: Verify Session in List") + fmt.Println("------------------------------") + + listOpts := types.NewListOptions().Size(10).Build() + sessionList, err := c.Sessions().List(ctx, listOpts) + if err != nil { + log.Fatalf("Failed to list sessions: %v", err) + } + + fmt.Printf(" Total sessions in project: %d\n", sessionList.Total) + for i, s := range sessionList.Items { + if i >= 5 { + fmt.Printf(" ... and %d more\n", len(sessionList.Items)-5) + break + } + fmt.Printf(" %d. %s (phase=%s, model=%s)\n", i+1, s.Name, phaseOrDefault(s.Phase), s.LlmModel) + } + fmt.Println() + + fmt.Println("Step 7: Stop Session") + fmt.Println("--------------------") + + stoppedSession, err := c.Sessions().Stop(ctx, createdSession.ID) + if err != nil { + fmt.Printf(" Warning: could not stop session: %v\n", err) + } else { + fmt.Printf(" Stop requested. Phase: %s\n", phaseOrDefault(stoppedSession.Phase)) + } + fmt.Println() + + fmt.Println("Complete!") + fmt.Println("=========") + fmt.Printf("Project: %s\n", projectName) + fmt.Printf("Session: %s (id=%s)\n", createdSession.Name, createdSession.ID) + fmt.Println("Open the Ambient UI to see the workspace and session.") +} + +func waitForPhase(ctx context.Context, c *client.Client, sessionID, targetPhase string, timeout time.Duration) (*types.Session, error) { + deadline := time.Now().Add(timeout) + poll := 3 * time.Second + + for time.Now().Before(deadline) { + session, err := c.Sessions().Get(ctx, sessionID) + if err != nil { + return nil, fmt.Errorf("failed to get session: %w", err) + } + + currentPhase := strings.ToLower(session.Phase) + target := strings.ToLower(targetPhase) + + if currentPhase == target { + return session, nil + } + + if currentPhase == "failed" || currentPhase == "completed" { + return session, fmt.Errorf("session reached terminal phase %q before %q", session.Phase, targetPhase) + } + + fmt.Printf(" Phase: %s (waiting for %s...)\n", phaseOrDefault(session.Phase), targetPhase) + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(poll): + } + } + + return nil, fmt.Errorf("timed out waiting for phase %q after %v", targetPhase, timeout) +} + +func sendMessageAndWaitForReply(ctx context.Context, aguiBaseURL, token, sessionID, message string) (string, error) { + runPayload := map[string]interface{}{ + "threadId": sessionID, + "messages": []map[string]interface{}{ + { + "id": fmt.Sprintf("msg-%d", time.Now().UnixNano()), + "role": "user", + "content": message, + }, + }, + } + + body, _ := json.Marshal(runPayload) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, aguiBaseURL+"/run", strings.NewReader(string(body))) + if err != nil { + return "", fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "text/event-stream") + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + httpClient := &http.Client{Timeout: 30 * time.Second} + resp, err := httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("send message: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("run endpoint returned %d", resp.StatusCode) + } + + var runResult map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&runResult); err != nil { + return "", fmt.Errorf("decode run response: %w", err) + } + + streamURL := aguiBaseURL + "/events" + if runID, ok := runResult["runId"].(string); ok { + streamURL += "?runId=" + runID + } + + return consumeEventStream(ctx, streamURL, token, 2*time.Minute) +} + +func consumeEventStream(ctx context.Context, streamURL, token string, timeout time.Duration) (string, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, streamURL, nil) + if err != nil { + return "", fmt.Errorf("create SSE request: %w", err) + } + req.Header.Set("Accept", "text/event-stream") + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + httpClient := &http.Client{Timeout: 0} + resp, err := httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("connect to event stream: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("event stream returned %d", resp.StatusCode) + } + + var reply strings.Builder + scanner := bufio.NewScanner(resp.Body) + + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "data: ") { + continue + } + + jsonData := strings.TrimPrefix(line, "data: ") + var event map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &event); err != nil { + continue + } + + eventType, _ := event["type"].(string) + + switch eventType { + case "TEXT_MESSAGE_CONTENT": + if delta, ok := event["delta"].(string); ok { + reply.WriteString(delta) + } + case "RUN_FINISHED", "RUN_ERROR": + return reply.String(), nil + } + } + + if reply.Len() > 0 { + return reply.String(), nil + } + return "", fmt.Errorf("event stream ended without a complete reply") +} + +func deriveAGUIBaseURL(apiURL, projectName, kubeCRName, sessionID string) string { + sessionName := kubeCRName + if sessionName == "" { + sessionName = sessionID + } + + baseURL := strings.TrimRight(apiURL, "/") + if strings.Contains(baseURL, "ambient-api-server") { + baseURL = strings.TrimSuffix(baseURL, "/api/ambient-api-server/v1") + } + + return fmt.Sprintf("%s/api/projects/%s/agentic-sessions/%s/agui", baseURL, projectName, sessionName) +} + +func phaseOrDefault(phase string) string { + if phase == "" { + return "Pending" + } + return phase +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} + +func asAPIError(err error, target **types.APIError) bool { + if err == nil { + return false + } + if e, ok := err.(*types.APIError); ok { + *target = e + return true + } + return false +} diff --git a/components/ambient-sdk/go-sdk/go.mod b/components/ambient-sdk/go-sdk/go.mod new file mode 100644 index 000000000..62b4459bd --- /dev/null +++ b/components/ambient-sdk/go-sdk/go.mod @@ -0,0 +1,3 @@ +module github.com/ambient-code/platform/components/ambient-sdk/go-sdk + +go 1.21 diff --git a/components/ambient-sdk/go-sdk/go.sum b/components/ambient-sdk/go-sdk/go.sum new file mode 100644 index 000000000..e69de29bb diff --git a/components/ambient-sdk/go-sdk/types/base.go b/components/ambient-sdk/go-sdk/types/base.go new file mode 100644 index 000000000..3039c2305 --- /dev/null +++ b/components/ambient-sdk/go-sdk/types/base.go @@ -0,0 +1,43 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package types + +import ( + "strconv" + "time" +) + +type ObjectReference struct { + ID string `json:"id,omitempty"` + Kind string `json:"kind,omitempty"` + Href string `json:"href,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +type ListMeta struct { + Kind string `json:"kind"` + Page int `json:"page"` + Size int `json:"size"` + Total int `json:"total"` +} + +type APIError struct { + ID string `json:"id,omitempty"` + Kind string `json:"kind,omitempty"` + Href string `json:"href,omitempty"` + Code string `json:"code"` + Reason string `json:"reason"` + OperationID string `json:"operation_id,omitempty"` + StatusCode int `json:"-"` +} + +func (e *APIError) Error() string { + if e.StatusCode > 0 { + return "ambient API error " + strconv.Itoa(e.StatusCode) + ": " + e.Code + " — " + e.Reason + } + return "ambient API error: " + e.Code + " — " + e.Reason +} diff --git a/components/ambient-sdk/go-sdk/types/list_options.go b/components/ambient-sdk/go-sdk/types/list_options.go new file mode 100644 index 000000000..487f43323 --- /dev/null +++ b/components/ambient-sdk/go-sdk/types/list_options.go @@ -0,0 +1,54 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package types + +type ListOptions struct { + Page int + Size int + Search string + OrderBy string + Fields string +} + +type ListOptionsBuilder struct { + opts ListOptions +} + +func NewListOptions() *ListOptionsBuilder { + return &ListOptionsBuilder{opts: ListOptions{Page: 1, Size: 100}} +} + +func (b *ListOptionsBuilder) Page(page int) *ListOptionsBuilder { + b.opts.Page = page + return b +} + +func (b *ListOptionsBuilder) Size(size int) *ListOptionsBuilder { + if size > 65500 { + size = 65500 + } + b.opts.Size = size + return b +} + +func (b *ListOptionsBuilder) Search(search string) *ListOptionsBuilder { + b.opts.Search = search + return b +} + +func (b *ListOptionsBuilder) OrderBy(orderBy string) *ListOptionsBuilder { + b.opts.OrderBy = orderBy + return b +} + +func (b *ListOptionsBuilder) Fields(fields string) *ListOptionsBuilder { + b.opts.Fields = fields + return b +} + +func (b *ListOptionsBuilder) Build() *ListOptions { + return &b.opts +} diff --git a/components/ambient-sdk/go-sdk/types/project.go b/components/ambient-sdk/go-sdk/types/project.go new file mode 100644 index 000000000..8a150e8fb --- /dev/null +++ b/components/ambient-sdk/go-sdk/types/project.go @@ -0,0 +1,123 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package types + +import ( + "errors" + "fmt" +) + +type Project struct { + ObjectReference + + Annotations string `json:"annotations,omitempty"` + Description string `json:"description,omitempty"` + DisplayName string `json:"display_name,omitempty"` + Labels string `json:"labels,omitempty"` + Name string `json:"name"` + Status string `json:"status,omitempty"` +} + +type ProjectList struct { + ListMeta + Items []Project `json:"items"` +} + +func (l *ProjectList) GetItems() []Project { return l.Items } +func (l *ProjectList) GetTotal() int { return l.Total } +func (l *ProjectList) GetPage() int { return l.Page } +func (l *ProjectList) GetSize() int { return l.Size } + +type ProjectBuilder struct { + resource Project + errors []error +} + +func NewProjectBuilder() *ProjectBuilder { + return &ProjectBuilder{} +} + +func (b *ProjectBuilder) Annotations(v string) *ProjectBuilder { + b.resource.Annotations = v + return b +} + +func (b *ProjectBuilder) Description(v string) *ProjectBuilder { + b.resource.Description = v + return b +} + +func (b *ProjectBuilder) DisplayName(v string) *ProjectBuilder { + b.resource.DisplayName = v + return b +} + +func (b *ProjectBuilder) Labels(v string) *ProjectBuilder { + b.resource.Labels = v + return b +} + +func (b *ProjectBuilder) Name(v string) *ProjectBuilder { + b.resource.Name = v + return b +} + +func (b *ProjectBuilder) Status(v string) *ProjectBuilder { + b.resource.Status = v + return b +} + +func (b *ProjectBuilder) Build() (*Project, error) { + if b.resource.Name == "" { + b.errors = append(b.errors, fmt.Errorf("name is required")) + } + if len(b.errors) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...)) + } + return &b.resource, nil +} + +type ProjectPatchBuilder struct { + patch map[string]any +} + +func NewProjectPatchBuilder() *ProjectPatchBuilder { + return &ProjectPatchBuilder{patch: make(map[string]any)} +} + +func (b *ProjectPatchBuilder) Annotations(v string) *ProjectPatchBuilder { + b.patch["annotations"] = v + return b +} + +func (b *ProjectPatchBuilder) Description(v string) *ProjectPatchBuilder { + b.patch["description"] = v + return b +} + +func (b *ProjectPatchBuilder) DisplayName(v string) *ProjectPatchBuilder { + b.patch["display_name"] = v + return b +} + +func (b *ProjectPatchBuilder) Labels(v string) *ProjectPatchBuilder { + b.patch["labels"] = v + return b +} + +func (b *ProjectPatchBuilder) Name(v string) *ProjectPatchBuilder { + b.patch["name"] = v + return b +} + +func (b *ProjectPatchBuilder) Status(v string) *ProjectPatchBuilder { + b.patch["status"] = v + return b +} + +func (b *ProjectPatchBuilder) Build() map[string]any { + return b.patch +} diff --git a/components/ambient-sdk/go-sdk/types/project_settings.go b/components/ambient-sdk/go-sdk/types/project_settings.go new file mode 100644 index 000000000..9cc139f8a --- /dev/null +++ b/components/ambient-sdk/go-sdk/types/project_settings.go @@ -0,0 +1,90 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package types + +import ( + "errors" + "fmt" +) + +type ProjectSettings struct { + ObjectReference + + GroupAccess string `json:"group_access,omitempty"` + ProjectID string `json:"project_id"` + Repositories string `json:"repositories,omitempty"` +} + +type ProjectSettingsList struct { + ListMeta + Items []ProjectSettings `json:"items"` +} + +func (l *ProjectSettingsList) GetItems() []ProjectSettings { return l.Items } +func (l *ProjectSettingsList) GetTotal() int { return l.Total } +func (l *ProjectSettingsList) GetPage() int { return l.Page } +func (l *ProjectSettingsList) GetSize() int { return l.Size } + +type ProjectSettingsBuilder struct { + resource ProjectSettings + errors []error +} + +func NewProjectSettingsBuilder() *ProjectSettingsBuilder { + return &ProjectSettingsBuilder{} +} + +func (b *ProjectSettingsBuilder) GroupAccess(v string) *ProjectSettingsBuilder { + b.resource.GroupAccess = v + return b +} + +func (b *ProjectSettingsBuilder) ProjectID(v string) *ProjectSettingsBuilder { + b.resource.ProjectID = v + return b +} + +func (b *ProjectSettingsBuilder) Repositories(v string) *ProjectSettingsBuilder { + b.resource.Repositories = v + return b +} + +func (b *ProjectSettingsBuilder) Build() (*ProjectSettings, error) { + if b.resource.ProjectID == "" { + b.errors = append(b.errors, fmt.Errorf("project_id is required")) + } + if len(b.errors) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...)) + } + return &b.resource, nil +} + +type ProjectSettingsPatchBuilder struct { + patch map[string]any +} + +func NewProjectSettingsPatchBuilder() *ProjectSettingsPatchBuilder { + return &ProjectSettingsPatchBuilder{patch: make(map[string]any)} +} + +func (b *ProjectSettingsPatchBuilder) GroupAccess(v string) *ProjectSettingsPatchBuilder { + b.patch["group_access"] = v + return b +} + +func (b *ProjectSettingsPatchBuilder) ProjectID(v string) *ProjectSettingsPatchBuilder { + b.patch["project_id"] = v + return b +} + +func (b *ProjectSettingsPatchBuilder) Repositories(v string) *ProjectSettingsPatchBuilder { + b.patch["repositories"] = v + return b +} + +func (b *ProjectSettingsPatchBuilder) Build() map[string]any { + return b.patch +} diff --git a/components/ambient-sdk/go-sdk/types/session.go b/components/ambient-sdk/go-sdk/types/session.go new file mode 100644 index 000000000..331d05c00 --- /dev/null +++ b/components/ambient-sdk/go-sdk/types/session.go @@ -0,0 +1,314 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package types + +import ( + "errors" + "fmt" + "time" +) + +type Session struct { + ObjectReference + + Annotations string `json:"annotations,omitempty"` + AssignedUserID string `json:"assigned_user_id,omitempty"` + BotAccountName string `json:"bot_account_name,omitempty"` + CompletionTime *time.Time `json:"completion_time,omitempty"` + Conditions string `json:"conditions,omitempty"` + CreatedByUserID string `json:"created_by_user_id,omitempty"` + EnvironmentVariables string `json:"environment_variables,omitempty"` + KubeCrName string `json:"kube_cr_name,omitempty"` + KubeCrUid string `json:"kube_cr_uid,omitempty"` + KubeNamespace string `json:"kube_namespace,omitempty"` + Labels string `json:"labels,omitempty"` + LlmMaxTokens int `json:"llm_max_tokens,omitempty"` + LlmModel string `json:"llm_model,omitempty"` + LlmTemperature float64 `json:"llm_temperature,omitempty"` + Name string `json:"name"` + ParentSessionID string `json:"parent_session_id,omitempty"` + Phase string `json:"phase,omitempty"` + ProjectID string `json:"project_id,omitempty"` + Prompt string `json:"prompt,omitempty"` + ReconciledRepos string `json:"reconciled_repos,omitempty"` + ReconciledWorkflow string `json:"reconciled_workflow,omitempty"` + RepoURL string `json:"repo_url,omitempty"` + Repos string `json:"repos,omitempty"` + ResourceOverrides string `json:"resource_overrides,omitempty"` + SdkRestartCount int `json:"sdk_restart_count,omitempty"` + SdkSessionID string `json:"sdk_session_id,omitempty"` + StartTime *time.Time `json:"start_time,omitempty"` + Timeout int `json:"timeout,omitempty"` + WorkflowID string `json:"workflow_id,omitempty"` +} + +type SessionList struct { + ListMeta + Items []Session `json:"items"` +} + +func (l *SessionList) GetItems() []Session { return l.Items } +func (l *SessionList) GetTotal() int { return l.Total } +func (l *SessionList) GetPage() int { return l.Page } +func (l *SessionList) GetSize() int { return l.Size } + +type SessionBuilder struct { + resource Session + errors []error +} + +func NewSessionBuilder() *SessionBuilder { + return &SessionBuilder{} +} + +func (b *SessionBuilder) Annotations(v string) *SessionBuilder { + b.resource.Annotations = v + return b +} + +func (b *SessionBuilder) AssignedUserID(v string) *SessionBuilder { + b.resource.AssignedUserID = v + return b +} + +func (b *SessionBuilder) BotAccountName(v string) *SessionBuilder { + b.resource.BotAccountName = v + return b +} + +func (b *SessionBuilder) EnvironmentVariables(v string) *SessionBuilder { + b.resource.EnvironmentVariables = v + return b +} + +func (b *SessionBuilder) Labels(v string) *SessionBuilder { + b.resource.Labels = v + return b +} + +func (b *SessionBuilder) LlmMaxTokens(v int) *SessionBuilder { + b.resource.LlmMaxTokens = v + return b +} + +func (b *SessionBuilder) LlmModel(v string) *SessionBuilder { + b.resource.LlmModel = v + return b +} + +func (b *SessionBuilder) LlmTemperature(v float64) *SessionBuilder { + b.resource.LlmTemperature = v + return b +} + +func (b *SessionBuilder) Name(v string) *SessionBuilder { + b.resource.Name = v + return b +} + +func (b *SessionBuilder) ParentSessionID(v string) *SessionBuilder { + b.resource.ParentSessionID = v + return b +} + +func (b *SessionBuilder) ProjectID(v string) *SessionBuilder { + b.resource.ProjectID = v + return b +} + +func (b *SessionBuilder) Prompt(v string) *SessionBuilder { + b.resource.Prompt = v + return b +} + +func (b *SessionBuilder) RepoURL(v string) *SessionBuilder { + b.resource.RepoURL = v + return b +} + +func (b *SessionBuilder) Repos(v string) *SessionBuilder { + b.resource.Repos = v + return b +} + +func (b *SessionBuilder) ResourceOverrides(v string) *SessionBuilder { + b.resource.ResourceOverrides = v + return b +} + +func (b *SessionBuilder) Timeout(v int) *SessionBuilder { + b.resource.Timeout = v + return b +} + +func (b *SessionBuilder) WorkflowID(v string) *SessionBuilder { + b.resource.WorkflowID = v + return b +} + +func (b *SessionBuilder) Build() (*Session, error) { + if b.resource.Name == "" { + b.errors = append(b.errors, fmt.Errorf("name is required")) + } + if len(b.errors) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...)) + } + return &b.resource, nil +} + +type SessionPatchBuilder struct { + patch map[string]any +} + +func NewSessionPatchBuilder() *SessionPatchBuilder { + return &SessionPatchBuilder{patch: make(map[string]any)} +} + +func (b *SessionPatchBuilder) Annotations(v string) *SessionPatchBuilder { + b.patch["annotations"] = v + return b +} + +func (b *SessionPatchBuilder) AssignedUserID(v string) *SessionPatchBuilder { + b.patch["assigned_user_id"] = v + return b +} + +func (b *SessionPatchBuilder) BotAccountName(v string) *SessionPatchBuilder { + b.patch["bot_account_name"] = v + return b +} + +func (b *SessionPatchBuilder) EnvironmentVariables(v string) *SessionPatchBuilder { + b.patch["environment_variables"] = v + return b +} + +func (b *SessionPatchBuilder) Labels(v string) *SessionPatchBuilder { + b.patch["labels"] = v + return b +} + +func (b *SessionPatchBuilder) LlmMaxTokens(v int) *SessionPatchBuilder { + b.patch["llm_max_tokens"] = v + return b +} + +func (b *SessionPatchBuilder) LlmModel(v string) *SessionPatchBuilder { + b.patch["llm_model"] = v + return b +} + +func (b *SessionPatchBuilder) LlmTemperature(v float64) *SessionPatchBuilder { + b.patch["llm_temperature"] = v + return b +} + +func (b *SessionPatchBuilder) Name(v string) *SessionPatchBuilder { + b.patch["name"] = v + return b +} + +func (b *SessionPatchBuilder) ParentSessionID(v string) *SessionPatchBuilder { + b.patch["parent_session_id"] = v + return b +} + +func (b *SessionPatchBuilder) Prompt(v string) *SessionPatchBuilder { + b.patch["prompt"] = v + return b +} + +func (b *SessionPatchBuilder) RepoURL(v string) *SessionPatchBuilder { + b.patch["repo_url"] = v + return b +} + +func (b *SessionPatchBuilder) Repos(v string) *SessionPatchBuilder { + b.patch["repos"] = v + return b +} + +func (b *SessionPatchBuilder) ResourceOverrides(v string) *SessionPatchBuilder { + b.patch["resource_overrides"] = v + return b +} + +func (b *SessionPatchBuilder) Timeout(v int) *SessionPatchBuilder { + b.patch["timeout"] = v + return b +} + +func (b *SessionPatchBuilder) WorkflowID(v string) *SessionPatchBuilder { + b.patch["workflow_id"] = v + return b +} + +func (b *SessionPatchBuilder) Build() map[string]any { + return b.patch +} + +type SessionStatusPatchBuilder struct { + patch map[string]any +} + +func NewSessionStatusPatchBuilder() *SessionStatusPatchBuilder { + return &SessionStatusPatchBuilder{patch: make(map[string]any)} +} + +func (b *SessionStatusPatchBuilder) CompletionTime(v *time.Time) *SessionStatusPatchBuilder { + b.patch["completion_time"] = v + return b +} + +func (b *SessionStatusPatchBuilder) Conditions(v string) *SessionStatusPatchBuilder { + b.patch["conditions"] = v + return b +} + +func (b *SessionStatusPatchBuilder) KubeCrUid(v string) *SessionStatusPatchBuilder { + b.patch["kube_cr_uid"] = v + return b +} + +func (b *SessionStatusPatchBuilder) KubeNamespace(v string) *SessionStatusPatchBuilder { + b.patch["kube_namespace"] = v + return b +} + +func (b *SessionStatusPatchBuilder) Phase(v string) *SessionStatusPatchBuilder { + b.patch["phase"] = v + return b +} + +func (b *SessionStatusPatchBuilder) ReconciledRepos(v string) *SessionStatusPatchBuilder { + b.patch["reconciled_repos"] = v + return b +} + +func (b *SessionStatusPatchBuilder) ReconciledWorkflow(v string) *SessionStatusPatchBuilder { + b.patch["reconciled_workflow"] = v + return b +} + +func (b *SessionStatusPatchBuilder) SdkRestartCount(v int) *SessionStatusPatchBuilder { + b.patch["sdk_restart_count"] = v + return b +} + +func (b *SessionStatusPatchBuilder) SdkSessionID(v string) *SessionStatusPatchBuilder { + b.patch["sdk_session_id"] = v + return b +} + +func (b *SessionStatusPatchBuilder) StartTime(v *time.Time) *SessionStatusPatchBuilder { + b.patch["start_time"] = v + return b +} + +func (b *SessionStatusPatchBuilder) Build() map[string]any { + return b.patch +} diff --git a/components/ambient-sdk/go-sdk/types/user.go b/components/ambient-sdk/go-sdk/types/user.go new file mode 100644 index 000000000..352df4eee --- /dev/null +++ b/components/ambient-sdk/go-sdk/types/user.go @@ -0,0 +1,93 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +package types + +import ( + "errors" + "fmt" +) + +type User struct { + ObjectReference + + Email string `json:"email,omitempty"` + Name string `json:"name"` + Username string `json:"username"` +} + +type UserList struct { + ListMeta + Items []User `json:"items"` +} + +func (l *UserList) GetItems() []User { return l.Items } +func (l *UserList) GetTotal() int { return l.Total } +func (l *UserList) GetPage() int { return l.Page } +func (l *UserList) GetSize() int { return l.Size } + +type UserBuilder struct { + resource User + errors []error +} + +func NewUserBuilder() *UserBuilder { + return &UserBuilder{} +} + +func (b *UserBuilder) Email(v string) *UserBuilder { + b.resource.Email = v + return b +} + +func (b *UserBuilder) Name(v string) *UserBuilder { + b.resource.Name = v + return b +} + +func (b *UserBuilder) Username(v string) *UserBuilder { + b.resource.Username = v + return b +} + +func (b *UserBuilder) Build() (*User, error) { + if b.resource.Name == "" { + b.errors = append(b.errors, fmt.Errorf("name is required")) + } + if b.resource.Username == "" { + b.errors = append(b.errors, fmt.Errorf("username is required")) + } + if len(b.errors) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...)) + } + return &b.resource, nil +} + +type UserPatchBuilder struct { + patch map[string]any +} + +func NewUserPatchBuilder() *UserPatchBuilder { + return &UserPatchBuilder{patch: make(map[string]any)} +} + +func (b *UserPatchBuilder) Email(v string) *UserPatchBuilder { + b.patch["email"] = v + return b +} + +func (b *UserPatchBuilder) Name(v string) *UserPatchBuilder { + b.patch["name"] = v + return b +} + +func (b *UserPatchBuilder) Username(v string) *UserPatchBuilder { + b.patch["username"] = v + return b +} + +func (b *UserPatchBuilder) Build() map[string]any { + return b.patch +} diff --git a/components/ambient-sdk/python-sdk/README.md b/components/ambient-sdk/python-sdk/README.md new file mode 100644 index 000000000..63e4144a7 --- /dev/null +++ b/components/ambient-sdk/python-sdk/README.md @@ -0,0 +1,470 @@ +# Ambient Platform Python SDK + +Simple HTTP client library for the Ambient Code Platform - Create and manage AI agent sessions without Kubernetes complexity. + +## Installation + +```bash +pip install ambient-platform-sdk +``` + +## Quick Start + +```python +import os +from ambient_platform import AmbientClient, CreateSessionRequest, RepoHTTP + +# Create HTTP client +client = AmbientClient( + base_url="https://your-platform.example.com", + token=os.getenv("AMBIENT_TOKEN"), # Bearer token + project=os.getenv("AMBIENT_PROJECT"), # Project namespace +) + +# Create a session +request = CreateSessionRequest( + task="Analyze the repository structure and provide a summary", + model="claude-3.5-sonnet", + repos=[ + RepoHTTP( + url="https://github.com/ambient-code/platform", + branch="main" + ) + ] +) + +response = client.create_session(request) +print(f"Created session: {response.id}") + +# Get session details +session = client.get_session(response.id) +print(f"Status: {session.status}") + +# List all sessions +sessions = client.list_sessions() +print(f"Found {len(sessions.items)} sessions") + +# Close client when done +client.close() +``` + +## Authentication & Authorization + +The SDK uses Bearer token authentication with project-scoped authorization: + +### Token Requirements + +- **Bearer Token**: Must be a valid authentication token (OpenShift, JWT, or GitHub format) +- **Project Header**: `X-Ambient-Project` specifies the target Kubernetes namespace +- **RBAC**: User must have appropriate permissions in the target namespace + +### Supported Token Formats + +- **OpenShift**: `sha256~...` format tokens from `oc whoami -t` +- **JWT**: Standard JSON Web Tokens with 3 base64 parts +- **GitHub**: Tokens starting with `ghp_`, `gho_`, `ghu_`, or `ghs_` + +### Required Permissions + +Your user account must have these Kubernetes RBAC permissions in the target project/namespace: + +```yaml +# Minimum required permissions +- apiGroups: ["vteam.ambient-code"] + resources: ["agenticsessions"] + verbs: ["get", "list", "create"] + +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get"] +``` + +### Common Permission Errors + +**403 Forbidden**: +```bash +# Check your permissions +oc auth can-i create agenticsessions.vteam.ambient-code -n your-project +oc auth can-i list agenticsessions.vteam.ambient-code -n your-project +``` + +**401 Unauthorized**: +```bash +# Check token validity +oc whoami # Should return your username +oc whoami -t # Should return a token starting with sha256~ +``` + +**400 Bad Request - Project required**: +- Ensure `AMBIENT_PROJECT` environment variable is set +- Project must be a valid Kubernetes namespace name +- User must have access to the specified project + +```bash +# Set environment variables +export AMBIENT_TOKEN="your-bearer-token" # Required +export AMBIENT_PROJECT="your-project-name" # Required +export AMBIENT_API_URL="https://your-api.com" # Optional +``` + +**OpenShift Users:** +```bash +# Use your OpenShift token +export AMBIENT_TOKEN="$(oc whoami -t)" +export AMBIENT_PROJECT="$(oc project -q)" +``` + +## Core Operations + +### Create Session + +```python +from ambient_platform import CreateSessionRequest, RepoHTTP + +request = CreateSessionRequest( + task="Review this code for security issues", + model="claude-3.5-sonnet", # Optional, uses platform default if omitted + repos=[ + RepoHTTP(url="https://github.com/user/repo", branch="main") + ] +) + +response = client.create_session(request) +print(f"Session ID: {response.id}") +``` + +### Get Session Details + +```python +from ambient_platform import StatusCompleted + +session = client.get_session("session-1234567") +print(f"Status: {session.status}") + +if session.status == StatusCompleted: + print(f"Result: {session.result}") +``` + +### List Sessions + +```python +sessions = client.list_sessions() +for session in sessions.items: + print(f"- {session.id} ({session.status}): {session.task}") +``` + +### Monitor Session Completion + +```python +# Wait for session to complete (with timeout) +try: + completed = client.wait_for_completion( + session_id="session-1234567", + poll_interval=5.0, # Check every 5 seconds + timeout=300.0 # 5 minute timeout + ) + + if completed.status == StatusCompleted: + print(f"Success: {completed.result}") + else: + print(f"Failed: {completed.error}") + +except TimeoutError: + print("Session monitoring timed out") +``` + +## Session Status Values + +```python +from ambient_platform import StatusPending, StatusRunning, StatusCompleted, StatusFailed + +# Status constants +StatusPending = "pending" # Session created, waiting to start +StatusRunning = "running" # AI agent actively working +StatusCompleted = "completed" # Task finished successfully +StatusFailed = "failed" # Task failed with error +``` + +## Configuration Options + +### Environment Variables + +Create client from environment variables: + +```python +# Automatically reads AMBIENT_API_URL, AMBIENT_TOKEN, AMBIENT_PROJECT +client = AmbientClient.from_env() +``` + +### Context Manager + +Use client as context manager for automatic cleanup: + +```python +with AmbientClient.from_env() as client: + response = client.create_session(request) + session = client.get_session(response.id) + # Client automatically closed when exiting context +``` + +### Custom Timeout + +```python +client = AmbientClient( + base_url="https://api.example.com", + token="your-token", + project="your-project", + timeout=60.0 # 60 second timeout +) +``` + +## Error Handling + +```python +from ambient_platform.exceptions import ( + AmbientAPIError, + AuthenticationError, + SessionNotFoundError, + AmbientConnectionError, +) + +try: + session = client.get_session("invalid-id") +except SessionNotFoundError as e: + print(f"Session not found: {e}") +except AuthenticationError as e: + print(f"Auth failed: {e}") +except AmbientConnectionError as e: + print(f"Connection failed: {e}") +except AmbientAPIError as e: + print(f"API error: {e}") +``` + +## Examples + +See the `examples/` directory for complete working examples: + +- **`main.py`** - Complete session lifecycle demonstration + +Run the example: +```bash +cd examples/ +export AMBIENT_TOKEN="your-token" +export AMBIENT_PROJECT="your-project" +python main.py +``` + +## API Reference + +### AmbientClient + +```python +class AmbientClient: + def __init__(self, base_url: str, token: str, project: str, timeout: float = 30.0) + + def create_session(self, request: CreateSessionRequest) -> CreateSessionResponse + def get_session(self, session_id: str) -> SessionResponse + def list_sessions(self) -> SessionListResponse + def wait_for_completion(self, session_id: str, poll_interval: float = 5.0, timeout: Optional[float] = None) -> SessionResponse + + @classmethod + def from_env(cls, **kwargs) -> "AmbientClient" + + def close(self) # Close HTTP client +``` + +### Data Classes + +```python +@dataclass +class CreateSessionRequest: + task: str + model: Optional[str] = None + repos: Optional[List[RepoHTTP]] = None + +@dataclass +class RepoHTTP: + url: str + branch: Optional[str] = None + +@dataclass +class SessionResponse: + id: str + status: str # "pending", "running", "completed", "failed" + task: str + model: Optional[str] = None + created_at: Optional[str] = None + completed_at: Optional[str] = None + result: Optional[str] = None + error: Optional[str] = None + +@dataclass +class SessionListResponse: + items: List[SessionResponse] + total: int + +@dataclass +class CreateSessionResponse: + id: str + message: str + +@dataclass +class ErrorResponse: + error: str + message: Optional[str] = None +``` + +## Architecture + +### Design Principles + +- **HTTP-First**: Pure REST API client with no Kubernetes dependencies +- **Minimal Dependencies**: Uses only `httpx` for HTTP requests +- **Simple Integration**: Easy to embed in any Python application +- **Type Safety**: Dataclasses with type hints for all requests/responses +- **Clear Separation**: Public SDK vs internal platform implementation + +### HTTP vs Kubernetes + +This SDK provides a **simplified HTTP interface** to the Ambient Platform: + +| Aspect | HTTP SDK (This Package) | Internal Platform | +|--------|------------------------|-------------------| +| **API** | Simple REST endpoints (`/v1/sessions`) | Complex Kubernetes CRDs | +| **Auth** | Bearer token + project header | RBAC + service accounts | +| **Types** | Simple dataclasses | Full K8s metadata/spec/status | +| **Usage** | Any HTTP client, any environment | Kubernetes cluster access required | +| **Dependencies** | Only `httpx` | `kubernetes`, `pydantic`, etc. | +| **Target** | External integrators, simple automation | Internal platform components | + +## Troubleshooting + +### Authentication Issues +``` +❌ AMBIENT_TOKEN environment variable is required +``` +**Solution**: Set your Bearer token: `export AMBIENT_TOKEN="your-token"` + +### Project Header Missing +``` +API error (400): Project required. Set X-Ambient-Project header +``` +**Solution**: Set project name: `export AMBIENT_PROJECT="your-project"` + +### Connection Errors +``` +Failed to connect to API: [connection error details] +``` +**Solution**: Verify API endpoint and network connectivity + +### Session Not Found +``` +session not found: session-xyz +``` +**Solution**: Verify session ID and check if you have access to the project + +## Development + +### Setup Development Environment + +```bash +# Clone repository +git clone https://github.com/ambient-code/platform.git +cd platform/components/ambient-sdk/python-sdk + +# Create virtual environment +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install in development mode +pip install -e ".[dev]" + +# Run tests +pytest + +# Format code +black ambient_platform examples +isort ambient_platform examples + +# Type checking +mypy ambient_platform +``` + +### Running Tests + +```bash +# Run all tests +pytest + +# Run integration tests (requires running API) +pytest -m integration + +# Run with coverage +pytest --cov=ambient_platform --cov-report=html +``` + +### Testing Against Real API + +```bash +# Set environment variables +export AMBIENT_TOKEN="your-token" +export AMBIENT_PROJECT="your-project" +export AMBIENT_API_URL="https://your-api.example.com" + +# Run example +python examples/main.py + +# Run integration tests +pytest -m integration +``` + +## OpenAPI Specification + +This SDK is built to match the canonical OpenAPI specification owned by the API server at `../../ambient-api-server/openapi/openapi.yaml`. The SDK does not maintain its own spec copy — types and client behavior derive from the API server's definitions. + +## Terminal Usage Guide + +### Quick Setup + +```bash +# Navigate to python-sdk directory +cd /path/to/platform/components/ambient-sdk/python-sdk + +# Create and activate virtual environment +python -m venv venv +source venv/bin/activate + +# Install SDK +pip install -e . + +# Set environment variables +export AMBIENT_TOKEN="your-bearer-token" +export AMBIENT_PROJECT="your-project-name" +export AMBIENT_API_URL="https://your-api-endpoint.com" # Optional + +# Run example +python examples/main.py +``` + +### Interactive Python Session + +```bash +# Start Python REPL +python + +# Use the SDK interactively +>>> from ambient_platform import AmbientClient, CreateSessionRequest, RepoHTTP +>>> client = AmbientClient.from_env() +>>> request = CreateSessionRequest(task="Hello world", model="claude-3.5-sonnet") +>>> response = client.create_session(request) +>>> print(f"Session ID: {response.id}") +``` + +## Contributing + +1. **SDK Changes**: Modify code in `ambient_platform/` directory +2. **API Changes**: Update `../openapi.yaml` specification first +3. **Examples**: Add working examples to `examples/` directory +4. **Testing**: Ensure all changes work with real API endpoints + +For complete platform documentation, see the main [platform repository](https://github.com/ambient-code/platform). \ No newline at end of file diff --git a/components/ambient-sdk/python-sdk/ambient_platform/__init__.py b/components/ambient-sdk/python-sdk/ambient_platform/__init__.py new file mode 100644 index 000000000..f3ff0a177 --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/__init__.py @@ -0,0 +1,30 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +"""Ambient Platform SDK for Python.""" + +from .client import AmbientClient +from ._base import APIError, ListOptions +from .project import Project, ProjectPatch +from .project_settings import ProjectSettings, ProjectSettingsPatch +from .session import Session, SessionPatch, SessionStatusPatch +from .user import User, UserPatch + +__version__ = "1.0.0" + +__all__ = [ + "AmbientClient", + "APIError", + "ListOptions", + "Project", + "ProjectPatch", + "ProjectSettings", + "ProjectSettingsPatch", + "Session", + "SessionPatch", + "SessionStatusPatch", + "User", + "UserPatch", +] \ No newline at end of file diff --git a/components/ambient-sdk/python-sdk/ambient_platform/_base.py b/components/ambient-sdk/python-sdk/ambient_platform/_base.py new file mode 100644 index 000000000..1a3ebc4a8 --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/_base.py @@ -0,0 +1,106 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Optional + + +def _parse_datetime(value: Any) -> Optional[datetime]: + if value is None: + return None + if isinstance(value, datetime): + return value + if isinstance(value, str): + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")) + except (ValueError, TypeError): + return None + return None + + +@dataclass(frozen=True) +class ObjectReference: + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + @classmethod + def from_dict(cls, data: dict) -> ObjectReference: + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), + ) + + +@dataclass(frozen=True) +class ListMeta: + kind: str = "" + page: int = 0 + size: int = 0 + total: int = 0 + + +@dataclass(frozen=True) +class APIError(Exception): + status_code: int = 0 + code: str = "" + reason: str = "" + operation_id: str = "" + id: str = "" + kind: str = "" + href: str = "" + + def __str__(self) -> str: + return f"ambient API error {self.status_code}: {self.code} — {self.reason}" + + @classmethod + def from_dict(cls, data: dict, status_code: int = 0) -> APIError: + return cls( + status_code=status_code, + code=data.get("code", ""), + reason=data.get("reason", ""), + operation_id=data.get("operation_id", ""), + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + ) + + +class ListOptions: + def __init__(self) -> None: + self._params: dict[str, Any] = {"page": 1, "size": 100} + + def page(self, value: int) -> ListOptions: + self._params["page"] = value + return self + + def size(self, value: int) -> ListOptions: + if value > 65500: + value = 65500 + self._params["size"] = value + return self + + def search(self, value: str) -> ListOptions: + self._params["search"] = value + return self + + def order_by(self, value: str) -> ListOptions: + self._params["orderBy"] = value + return self + + def fields(self, value: str) -> ListOptions: + self._params["fields"] = value + return self + + def to_params(self) -> dict[str, Any]: + return {k: v for k, v in self._params.items() if v} diff --git a/components/ambient-sdk/python-sdk/ambient_platform/_iterator.py b/components/ambient-sdk/python-sdk/ambient_platform/_iterator.py new file mode 100644 index 000000000..1e18b742a --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/_iterator.py @@ -0,0 +1,20 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from typing import Any, Callable, Iterator, TypeVar + +T = TypeVar("T") + + +def paginate(fetch_page: Callable[..., Any], size: int = 100) -> Iterator[Any]: + page = 1 + while True: + result = fetch_page(page=page, size=size) + yield from result.items + if page * size >= result.total: + break + page += 1 diff --git a/components/ambient-sdk/python-sdk/ambient_platform/_project_api.py b/components/ambient-sdk/python-sdk/ambient_platform/_project_api.py new file mode 100644 index 000000000..a1f763123 --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/_project_api.py @@ -0,0 +1,48 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from typing import Any, Iterator, Optional, TYPE_CHECKING + +from ._base import ListOptions +from .project import Project, ProjectList + +if TYPE_CHECKING: + from .client import AmbientClient + + +class ProjectAPI: + def __init__(self, client: AmbientClient) -> None: + self._client = client + + def create(self, data: dict) -> Project: + resp = self._client._request("POST", "/projects", json=data) + return Project.from_dict(resp) + + def get(self, resource_id: str) -> Project: + resp = self._client._request("GET", f"/projects/{resource_id}") + return Project.from_dict(resp) + + def list(self, opts: Optional[ListOptions] = None) -> ProjectList: + params = opts.to_params() if opts else None + resp = self._client._request("GET", "/projects", params=params) + return ProjectList.from_dict(resp) + def update(self, resource_id: str, patch: Any) -> Project: + data = patch.to_dict() if hasattr(patch, "to_dict") else patch + resp = self._client._request("PATCH", f"/projects/{resource_id}", json=data) + return Project.from_dict(resp) + + def delete(self, resource_id: str) -> None: + self._client._request("DELETE", f"/projects/{resource_id}", expect_json=False) + + def list_all(self, size: int = 100, **kwargs: Any) -> Iterator[Project]: + page = 1 + while True: + result = self.list(ListOptions().page(page).size(size)) + yield from result.items + if page * size >= result.total: + break + page += 1 diff --git a/components/ambient-sdk/python-sdk/ambient_platform/_project_settings_api.py b/components/ambient-sdk/python-sdk/ambient_platform/_project_settings_api.py new file mode 100644 index 000000000..05bfc15bc --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/_project_settings_api.py @@ -0,0 +1,48 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from typing import Any, Iterator, Optional, TYPE_CHECKING + +from ._base import ListOptions +from .project_settings import ProjectSettings, ProjectSettingsList + +if TYPE_CHECKING: + from .client import AmbientClient + + +class ProjectSettingsAPI: + def __init__(self, client: AmbientClient) -> None: + self._client = client + + def create(self, data: dict) -> ProjectSettings: + resp = self._client._request("POST", "/project_settings", json=data) + return ProjectSettings.from_dict(resp) + + def get(self, resource_id: str) -> ProjectSettings: + resp = self._client._request("GET", f"/project_settings/{resource_id}") + return ProjectSettings.from_dict(resp) + + def list(self, opts: Optional[ListOptions] = None) -> ProjectSettingsList: + params = opts.to_params() if opts else None + resp = self._client._request("GET", "/project_settings", params=params) + return ProjectSettingsList.from_dict(resp) + def update(self, resource_id: str, patch: Any) -> ProjectSettings: + data = patch.to_dict() if hasattr(patch, "to_dict") else patch + resp = self._client._request("PATCH", f"/project_settings/{resource_id}", json=data) + return ProjectSettings.from_dict(resp) + + def delete(self, resource_id: str) -> None: + self._client._request("DELETE", f"/project_settings/{resource_id}", expect_json=False) + + def list_all(self, size: int = 100, **kwargs: Any) -> Iterator[ProjectSettings]: + page = 1 + while True: + result = self.list(ListOptions().page(page).size(size)) + yield from result.items + if page * size >= result.total: + break + page += 1 diff --git a/components/ambient-sdk/python-sdk/ambient_platform/_session_api.py b/components/ambient-sdk/python-sdk/ambient_platform/_session_api.py new file mode 100644 index 000000000..6e3f033db --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/_session_api.py @@ -0,0 +1,58 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from typing import Any, Iterator, Optional, TYPE_CHECKING + +from ._base import ListOptions +from .session import Session, SessionList, SessionStatusPatch + +if TYPE_CHECKING: + from .client import AmbientClient + + +class SessionAPI: + def __init__(self, client: AmbientClient) -> None: + self._client = client + + def create(self, data: dict) -> Session: + resp = self._client._request("POST", "/sessions", json=data) + return Session.from_dict(resp) + + def get(self, resource_id: str) -> Session: + resp = self._client._request("GET", f"/sessions/{resource_id}") + return Session.from_dict(resp) + + def list(self, opts: Optional[ListOptions] = None) -> SessionList: + params = opts.to_params() if opts else None + resp = self._client._request("GET", "/sessions", params=params) + return SessionList.from_dict(resp) + def update(self, resource_id: str, patch: Any) -> Session: + data = patch.to_dict() if hasattr(patch, "to_dict") else patch + resp = self._client._request("PATCH", f"/sessions/{resource_id}", json=data) + return Session.from_dict(resp) + + def update_status(self, resource_id: str, patch: Any) -> Session: + data = patch.to_dict() if hasattr(patch, "to_dict") else patch + resp = self._client._request("PATCH", f"/sessions/{resource_id}/status", json=data) + return Session.from_dict(resp) + + def start(self, resource_id: str) -> Session: + resp = self._client._request("POST", f"/sessions/{resource_id}/start") + return Session.from_dict(resp) + + def stop(self, resource_id: str) -> Session: + resp = self._client._request("POST", f"/sessions/{resource_id}/stop") + return Session.from_dict(resp) + + def list_all(self, size: int = 100, **kwargs: Any) -> Iterator[Session]: + page = 1 + while True: + result = self.list(ListOptions().page(page).size(size)) + yield from result.items + if page * size >= result.total: + break + page += 1 diff --git a/components/ambient-sdk/python-sdk/ambient_platform/_user_api.py b/components/ambient-sdk/python-sdk/ambient_platform/_user_api.py new file mode 100644 index 000000000..f55a4b1e3 --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/_user_api.py @@ -0,0 +1,45 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from typing import Any, Iterator, Optional, TYPE_CHECKING + +from ._base import ListOptions +from .user import User, UserList + +if TYPE_CHECKING: + from .client import AmbientClient + + +class UserAPI: + def __init__(self, client: AmbientClient) -> None: + self._client = client + + def create(self, data: dict) -> User: + resp = self._client._request("POST", "/users", json=data) + return User.from_dict(resp) + + def get(self, resource_id: str) -> User: + resp = self._client._request("GET", f"/users/{resource_id}") + return User.from_dict(resp) + + def list(self, opts: Optional[ListOptions] = None) -> UserList: + params = opts.to_params() if opts else None + resp = self._client._request("GET", "/users", params=params) + return UserList.from_dict(resp) + def update(self, resource_id: str, patch: Any) -> User: + data = patch.to_dict() if hasattr(patch, "to_dict") else patch + resp = self._client._request("PATCH", f"/users/{resource_id}", json=data) + return User.from_dict(resp) + + def list_all(self, size: int = 100, **kwargs: Any) -> Iterator[User]: + page = 1 + while True: + result = self.list(ListOptions().page(page).size(size)) + yield from result.items + if page * size >= result.total: + break + page += 1 diff --git a/components/ambient-sdk/python-sdk/ambient_platform/client.py b/components/ambient-sdk/python-sdk/ambient_platform/client.py new file mode 100644 index 000000000..bfa66db9e --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/client.py @@ -0,0 +1,199 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +import json +import os +import re +from typing import Any, Optional, Union, TYPE_CHECKING +from urllib.parse import urlparse + +import httpx + +from ._base import APIError, ListOptions + +if TYPE_CHECKING: + from ._project_api import ProjectAPI + from ._project_settings_api import ProjectSettingsAPI + from ._session_api import SessionAPI + from ._user_api import UserAPI + + +class AmbientClient: + """HTTP client for the Ambient Platform API.""" + + _base_path = "/api/ambient-api-server/v1" + + def __init__( + self, + base_url: str, + token: str, + project: str, + *, + timeout: float = 30.0, + user_agent: str = "ambient-python-sdk/1.0.0", + ) -> None: + self._base_url = base_url.rstrip("/") + self._token = token + self._project = project + self._timeout = timeout + self._user_agent = user_agent + + self._validate_config() + + self._client = httpx.Client( + timeout=timeout, + headers={ + "User-Agent": user_agent, + "Accept": "application/json", + }, + ) + + # Initialize API interfaces + self._project_api: Optional[ProjectAPI] = None + self._project_settings_api: Optional[ProjectSettingsAPI] = None + self._session_api: Optional[SessionAPI] = None + self._user_api: Optional[UserAPI] = None + + @classmethod + def from_env(cls, **kwargs: Any) -> AmbientClient: + """Create client from environment variables.""" + base_url = os.environ.get("AMBIENT_API_URL", "http://localhost:8080") + token = os.environ.get("AMBIENT_TOKEN") + project = os.environ.get("AMBIENT_PROJECT") + + if not token: + raise ValueError("AMBIENT_TOKEN environment variable is required") + if not project: + raise ValueError("AMBIENT_PROJECT environment variable is required") + + return cls(base_url=base_url, token=token, project=project, **kwargs) + + def _validate_config(self) -> None: + """Validate client configuration.""" + if not self._base_url: + raise ValueError("base URL cannot be empty") + + if "example.com" in self._base_url or "placeholder" in self._base_url: + raise ValueError("placeholder domain is not allowed") + + parsed = urlparse(self._base_url) + if parsed.scheme not in ("http", "https"): + raise ValueError("URL scheme must be http or https") + + if not self._token: + raise ValueError("token cannot be empty") + + if len(self._token) < 20: + raise ValueError("token is too short (minimum 20 characters)") + + if self._token in ("YOUR_TOKEN_HERE", "PLACEHOLDER_TOKEN"): + raise ValueError("placeholder token is not allowed") + + if not self._project: + raise ValueError("project cannot be empty") + + if len(self._project) > 63: + raise ValueError("project name cannot exceed 63 characters") + + if not re.match(r'^[a-z0-9_-]+$', self._project): + raise ValueError("project name must contain only lowercase alphanumeric characters, hyphens, and underscores") + + def _request( + self, + method: str, + path: str, + *, + json: Optional[dict[str, Any]] = None, + params: Optional[dict[str, Any]] = None, + expect_json: bool = True, + ) -> Any: + """Make HTTP request to the API.""" + url = self._base_url + "/api/ambient-api-server/v1" + path + + headers = { + "Authorization": f"Bearer {self._token}", + "X-Ambient-Project": self._project, + } + + if json is not None: + headers["Content-Type"] = "application/json" + + try: + response = self._client.request( + method=method, + url=url, + headers=headers, + json=json, + params=params, + ) + + self._handle_response(response, expect_json) + + if expect_json and response.content: + return response.json() + + return None + + except httpx.RequestError as e: + raise APIError(f"Request failed: {e}") from e + + def _handle_response(self, response: httpx.Response, expect_json: bool) -> None: + """Handle HTTP response, raising appropriate errors.""" + if response.is_success: + return + + try: + if response.content and expect_json: + error_data = response.json() + if isinstance(error_data, dict) and "code" in error_data: + raise APIError.from_dict(error_data, response.status_code) + except (json.JSONDecodeError, ValueError): + pass + + # Fall back to generic error + raise APIError.from_dict( + {"reason": f"HTTP {response.status_code}: {response.reason_phrase}"}, + response.status_code + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._client.close() + + def __enter__(self) -> AmbientClient: + return self + + def __exit__(self, *args: Any) -> None: + self.close() + @property + def projects(self) -> ProjectAPI: + """Get the Project API interface.""" + if self._project_api is None: + from ._project_api import ProjectAPI + self._project_api = ProjectAPI(self) + return self._project_api + @property + def project_settings(self) -> ProjectSettingsAPI: + """Get the ProjectSettings API interface.""" + if self._project_settings_api is None: + from ._project_settings_api import ProjectSettingsAPI + self._project_settings_api = ProjectSettingsAPI(self) + return self._project_settings_api + @property + def sessions(self) -> SessionAPI: + """Get the Session API interface.""" + if self._session_api is None: + from ._session_api import SessionAPI + self._session_api = SessionAPI(self) + return self._session_api + @property + def users(self) -> UserAPI: + """Get the User API interface.""" + if self._user_api is None: + from ._user_api import UserAPI + self._user_api = UserAPI(self) + return self._user_api \ No newline at end of file diff --git a/components/ambient-sdk/python-sdk/ambient_platform/project.py b/components/ambient-sdk/python-sdk/ambient_platform/project.py new file mode 100644 index 000000000..114b664d5 --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/project.py @@ -0,0 +1,135 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Optional + +from ._base import ListMeta, _parse_datetime + + +@dataclass(frozen=True) +class Project: + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + annotations: str = "" + description: str = "" + display_name: str = "" + labels: str = "" + name: str = "" + status: str = "" + + @classmethod + def from_dict(cls, data: dict) -> Project: + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), + annotations=data.get("annotations", ""), + description=data.get("description", ""), + display_name=data.get("display_name", ""), + labels=data.get("labels", ""), + name=data.get("name", ""), + status=data.get("status", ""), + ) + + @classmethod + def builder(cls) -> ProjectBuilder: + return ProjectBuilder() + + +@dataclass(frozen=True) +class ProjectList: + kind: str = "" + page: int = 0 + size: int = 0 + total: int = 0 + items: list[Project] = () + + @classmethod + def from_dict(cls, data: dict) -> ProjectList: + return cls( + kind=data.get("kind", ""), + page=data.get("page", 0), + size=data.get("size", 0), + total=data.get("total", 0), + items=[Project.from_dict(item) for item in data.get("items", [])], + ) + + +class ProjectBuilder: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def annotations(self, value: str) -> ProjectBuilder: + self._data["annotations"] = value + return self + + def description(self, value: str) -> ProjectBuilder: + self._data["description"] = value + return self + + def display_name(self, value: str) -> ProjectBuilder: + self._data["display_name"] = value + return self + + def labels(self, value: str) -> ProjectBuilder: + self._data["labels"] = value + return self + + def name(self, value: str) -> ProjectBuilder: + self._data["name"] = value + return self + + def status(self, value: str) -> ProjectBuilder: + self._data["status"] = value + return self + + def build(self) -> dict: + if "name" not in self._data: + raise ValueError("name is required") + return dict(self._data) + + +class ProjectPatch: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def annotations(self, value: str) -> ProjectPatch: + self._data["annotations"] = value + return self + + def description(self, value: str) -> ProjectPatch: + self._data["description"] = value + return self + + def display_name(self, value: str) -> ProjectPatch: + self._data["display_name"] = value + return self + + def labels(self, value: str) -> ProjectPatch: + self._data["labels"] = value + return self + + def name(self, value: str) -> ProjectPatch: + self._data["name"] = value + return self + + def status(self, value: str) -> ProjectPatch: + self._data["status"] = value + return self + + def to_dict(self) -> dict: + return dict(self._data) + diff --git a/components/ambient-sdk/python-sdk/ambient_platform/project_settings.py b/components/ambient-sdk/python-sdk/ambient_platform/project_settings.py new file mode 100644 index 000000000..0f4279193 --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/project_settings.py @@ -0,0 +1,105 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Optional + +from ._base import ListMeta, _parse_datetime + + +@dataclass(frozen=True) +class ProjectSettings: + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + group_access: str = "" + project_id: str = "" + repositories: str = "" + + @classmethod + def from_dict(cls, data: dict) -> ProjectSettings: + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), + group_access=data.get("group_access", ""), + project_id=data.get("project_id", ""), + repositories=data.get("repositories", ""), + ) + + @classmethod + def builder(cls) -> ProjectSettingsBuilder: + return ProjectSettingsBuilder() + + +@dataclass(frozen=True) +class ProjectSettingsList: + kind: str = "" + page: int = 0 + size: int = 0 + total: int = 0 + items: list[ProjectSettings] = () + + @classmethod + def from_dict(cls, data: dict) -> ProjectSettingsList: + return cls( + kind=data.get("kind", ""), + page=data.get("page", 0), + size=data.get("size", 0), + total=data.get("total", 0), + items=[ProjectSettings.from_dict(item) for item in data.get("items", [])], + ) + + +class ProjectSettingsBuilder: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def group_access(self, value: str) -> ProjectSettingsBuilder: + self._data["group_access"] = value + return self + + def project_id(self, value: str) -> ProjectSettingsBuilder: + self._data["project_id"] = value + return self + + def repositories(self, value: str) -> ProjectSettingsBuilder: + self._data["repositories"] = value + return self + + def build(self) -> dict: + if "project_id" not in self._data: + raise ValueError("project_id is required") + return dict(self._data) + + +class ProjectSettingsPatch: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def group_access(self, value: str) -> ProjectSettingsPatch: + self._data["group_access"] = value + return self + + def project_id(self, value: str) -> ProjectSettingsPatch: + self._data["project_id"] = value + return self + + def repositories(self, value: str) -> ProjectSettingsPatch: + self._data["repositories"] = value + return self + + def to_dict(self) -> dict: + return dict(self._data) + diff --git a/components/ambient-sdk/python-sdk/ambient_platform/session.py b/components/ambient-sdk/python-sdk/ambient_platform/session.py new file mode 100644 index 000000000..5f290423b --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/session.py @@ -0,0 +1,314 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Optional + +from ._base import ListMeta, _parse_datetime + + +@dataclass(frozen=True) +class Session: + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + annotations: str = "" + assigned_user_id: str = "" + bot_account_name: str = "" + completion_time: Optional[datetime] = None + conditions: str = "" + created_by_user_id: str = "" + environment_variables: str = "" + kube_cr_name: str = "" + kube_cr_uid: str = "" + kube_namespace: str = "" + labels: str = "" + llm_max_tokens: int = 0 + llm_model: str = "" + llm_temperature: float = 0.0 + name: str = "" + parent_session_id: str = "" + phase: str = "" + project_id: str = "" + prompt: str = "" + reconciled_repos: str = "" + reconciled_workflow: str = "" + repo_url: str = "" + repos: str = "" + resource_overrides: str = "" + sdk_restart_count: int = 0 + sdk_session_id: str = "" + start_time: Optional[datetime] = None + timeout: int = 0 + workflow_id: str = "" + + @classmethod + def from_dict(cls, data: dict) -> Session: + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), + annotations=data.get("annotations", ""), + assigned_user_id=data.get("assigned_user_id", ""), + bot_account_name=data.get("bot_account_name", ""), + completion_time=_parse_datetime(data.get("completion_time")), + conditions=data.get("conditions", ""), + created_by_user_id=data.get("created_by_user_id", ""), + environment_variables=data.get("environment_variables", ""), + kube_cr_name=data.get("kube_cr_name", ""), + kube_cr_uid=data.get("kube_cr_uid", ""), + kube_namespace=data.get("kube_namespace", ""), + labels=data.get("labels", ""), + llm_max_tokens=data.get("llm_max_tokens", 0), + llm_model=data.get("llm_model", ""), + llm_temperature=data.get("llm_temperature", 0.0), + name=data.get("name", ""), + parent_session_id=data.get("parent_session_id", ""), + phase=data.get("phase", ""), + project_id=data.get("project_id", ""), + prompt=data.get("prompt", ""), + reconciled_repos=data.get("reconciled_repos", ""), + reconciled_workflow=data.get("reconciled_workflow", ""), + repo_url=data.get("repo_url", ""), + repos=data.get("repos", ""), + resource_overrides=data.get("resource_overrides", ""), + sdk_restart_count=data.get("sdk_restart_count", 0), + sdk_session_id=data.get("sdk_session_id", ""), + start_time=_parse_datetime(data.get("start_time")), + timeout=data.get("timeout", 0), + workflow_id=data.get("workflow_id", ""), + ) + + @classmethod + def builder(cls) -> SessionBuilder: + return SessionBuilder() + + +@dataclass(frozen=True) +class SessionList: + kind: str = "" + page: int = 0 + size: int = 0 + total: int = 0 + items: list[Session] = () + + @classmethod + def from_dict(cls, data: dict) -> SessionList: + return cls( + kind=data.get("kind", ""), + page=data.get("page", 0), + size=data.get("size", 0), + total=data.get("total", 0), + items=[Session.from_dict(item) for item in data.get("items", [])], + ) + + +class SessionBuilder: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def annotations(self, value: str) -> SessionBuilder: + self._data["annotations"] = value + return self + + def assigned_user_id(self, value: str) -> SessionBuilder: + self._data["assigned_user_id"] = value + return self + + def bot_account_name(self, value: str) -> SessionBuilder: + self._data["bot_account_name"] = value + return self + + def environment_variables(self, value: str) -> SessionBuilder: + self._data["environment_variables"] = value + return self + + def labels(self, value: str) -> SessionBuilder: + self._data["labels"] = value + return self + + def llm_max_tokens(self, value: int) -> SessionBuilder: + self._data["llm_max_tokens"] = value + return self + + def llm_model(self, value: str) -> SessionBuilder: + self._data["llm_model"] = value + return self + + def llm_temperature(self, value: float) -> SessionBuilder: + self._data["llm_temperature"] = value + return self + + def name(self, value: str) -> SessionBuilder: + self._data["name"] = value + return self + + def parent_session_id(self, value: str) -> SessionBuilder: + self._data["parent_session_id"] = value + return self + + def project_id(self, value: str) -> SessionBuilder: + self._data["project_id"] = value + return self + + def prompt(self, value: str) -> SessionBuilder: + self._data["prompt"] = value + return self + + def repo_url(self, value: str) -> SessionBuilder: + self._data["repo_url"] = value + return self + + def repos(self, value: str) -> SessionBuilder: + self._data["repos"] = value + return self + + def resource_overrides(self, value: str) -> SessionBuilder: + self._data["resource_overrides"] = value + return self + + def timeout(self, value: int) -> SessionBuilder: + self._data["timeout"] = value + return self + + def workflow_id(self, value: str) -> SessionBuilder: + self._data["workflow_id"] = value + return self + + def build(self) -> dict: + if "name" not in self._data: + raise ValueError("name is required") + return dict(self._data) + + +class SessionPatch: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def annotations(self, value: str) -> SessionPatch: + self._data["annotations"] = value + return self + + def assigned_user_id(self, value: str) -> SessionPatch: + self._data["assigned_user_id"] = value + return self + + def bot_account_name(self, value: str) -> SessionPatch: + self._data["bot_account_name"] = value + return self + + def environment_variables(self, value: str) -> SessionPatch: + self._data["environment_variables"] = value + return self + + def labels(self, value: str) -> SessionPatch: + self._data["labels"] = value + return self + + def llm_max_tokens(self, value: int) -> SessionPatch: + self._data["llm_max_tokens"] = value + return self + + def llm_model(self, value: str) -> SessionPatch: + self._data["llm_model"] = value + return self + + def llm_temperature(self, value: float) -> SessionPatch: + self._data["llm_temperature"] = value + return self + + def name(self, value: str) -> SessionPatch: + self._data["name"] = value + return self + + def parent_session_id(self, value: str) -> SessionPatch: + self._data["parent_session_id"] = value + return self + + def prompt(self, value: str) -> SessionPatch: + self._data["prompt"] = value + return self + + def repo_url(self, value: str) -> SessionPatch: + self._data["repo_url"] = value + return self + + def repos(self, value: str) -> SessionPatch: + self._data["repos"] = value + return self + + def resource_overrides(self, value: str) -> SessionPatch: + self._data["resource_overrides"] = value + return self + + def timeout(self, value: int) -> SessionPatch: + self._data["timeout"] = value + return self + + def workflow_id(self, value: str) -> SessionPatch: + self._data["workflow_id"] = value + return self + + def to_dict(self) -> dict: + return dict(self._data) + + +class SessionStatusPatch: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def completion_time(self, value: Optional[datetime]) -> SessionStatusPatch: + self._data["completion_time"] = value + return self + + def conditions(self, value: str) -> SessionStatusPatch: + self._data["conditions"] = value + return self + + def kube_cr_uid(self, value: str) -> SessionStatusPatch: + self._data["kube_cr_uid"] = value + return self + + def kube_namespace(self, value: str) -> SessionStatusPatch: + self._data["kube_namespace"] = value + return self + + def phase(self, value: str) -> SessionStatusPatch: + self._data["phase"] = value + return self + + def reconciled_repos(self, value: str) -> SessionStatusPatch: + self._data["reconciled_repos"] = value + return self + + def reconciled_workflow(self, value: str) -> SessionStatusPatch: + self._data["reconciled_workflow"] = value + return self + + def sdk_restart_count(self, value: int) -> SessionStatusPatch: + self._data["sdk_restart_count"] = value + return self + + def sdk_session_id(self, value: str) -> SessionStatusPatch: + self._data["sdk_session_id"] = value + return self + + def start_time(self, value: Optional[datetime]) -> SessionStatusPatch: + self._data["start_time"] = value + return self + + def to_dict(self) -> dict: + return dict(self._data) + diff --git a/components/ambient-sdk/python-sdk/ambient_platform/user.py b/components/ambient-sdk/python-sdk/ambient_platform/user.py new file mode 100644 index 000000000..1a144991e --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/user.py @@ -0,0 +1,107 @@ +# Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +# Source: ../../ambient-api-server/openapi/openapi.yaml +# Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +# Generated: 2026-02-26T15:57:52Z + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Optional + +from ._base import ListMeta, _parse_datetime + + +@dataclass(frozen=True) +class User: + id: str = "" + kind: str = "" + href: str = "" + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + email: str = "" + name: str = "" + username: str = "" + + @classmethod + def from_dict(cls, data: dict) -> User: + return cls( + id=data.get("id", ""), + kind=data.get("kind", ""), + href=data.get("href", ""), + created_at=_parse_datetime(data.get("created_at")), + updated_at=_parse_datetime(data.get("updated_at")), + email=data.get("email", ""), + name=data.get("name", ""), + username=data.get("username", ""), + ) + + @classmethod + def builder(cls) -> UserBuilder: + return UserBuilder() + + +@dataclass(frozen=True) +class UserList: + kind: str = "" + page: int = 0 + size: int = 0 + total: int = 0 + items: list[User] = () + + @classmethod + def from_dict(cls, data: dict) -> UserList: + return cls( + kind=data.get("kind", ""), + page=data.get("page", 0), + size=data.get("size", 0), + total=data.get("total", 0), + items=[User.from_dict(item) for item in data.get("items", [])], + ) + + +class UserBuilder: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def email(self, value: str) -> UserBuilder: + self._data["email"] = value + return self + + def name(self, value: str) -> UserBuilder: + self._data["name"] = value + return self + + def username(self, value: str) -> UserBuilder: + self._data["username"] = value + return self + + def build(self) -> dict: + if "name" not in self._data: + raise ValueError("name is required") + if "username" not in self._data: + raise ValueError("username is required") + return dict(self._data) + + +class UserPatch: + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + + def email(self, value: str) -> UserPatch: + self._data["email"] = value + return self + + def name(self, value: str) -> UserPatch: + self._data["name"] = value + return self + + def username(self, value: str) -> UserPatch: + self._data["username"] = value + return self + + def to_dict(self) -> dict: + return dict(self._data) + diff --git a/components/ambient-sdk/python-sdk/examples/main.py b/components/ambient-sdk/python-sdk/examples/main.py new file mode 100644 index 000000000..391d4ec65 --- /dev/null +++ b/components/ambient-sdk/python-sdk/examples/main.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Ambient Platform SDK - Python Example""" + +import sys + +from ambient_platform import ( + AmbientClient, + Session, + SessionPatch, + ListOptions, + APIError, +) + + +def main(): + print("Ambient Platform SDK - Python Example") + print("======================================") + + try: + client = AmbientClient.from_env(timeout=60.0) + except ValueError as e: + print(f"Configuration error: {e}") + sys.exit(1) + + with client: + data = Session.builder().name("example-session").prompt("Analyze the repository structure").build() + created = client.sessions.create(data) + print(f"Created session: {created.name} (id={created.id})") + + got = client.sessions.get(created.id) + print(f"Got session: {got.name}") + + sessions = client.sessions.list(ListOptions().size(10)) + print(f"Found {len(sessions.items)} sessions (total: {sessions.total})") + + patch = SessionPatch().prompt("Updated prompt") + updated = client.sessions.update(created.id, patch) + print(f"Updated session prompt: {updated.prompt}") + + print("\nIterating all sessions:") + count = 0 + for s in client.sessions.list_all(size=100): + count += 1 + if count <= 3: + print(f" {count}. {s.name} ({s.id})") + if count > 3: + print(f" ... and {count - 3} more") + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/components/ambient-sdk/python-sdk/pyproject.toml b/components/ambient-sdk/python-sdk/pyproject.toml new file mode 100644 index 000000000..6fad4126f --- /dev/null +++ b/components/ambient-sdk/python-sdk/pyproject.toml @@ -0,0 +1,123 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ambient-platform-sdk" +version = "1.0.0" +description = "Simple HTTP client library for the Ambient Code Platform - Create and manage AI agent sessions without Kubernetes complexity." +readme = "README.md" +license = {text = "Apache-2.0"} +authors = [ + {name = "Ambient Code Platform", email = "hello@ambient-code.io"} +] +maintainers = [ + {name = "Ambient Code Platform", email = "hello@ambient-code.io"} +] +keywords = [ + "ai", "automation", "http", "claude", "agents", "sdk", "api" +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", +] +requires-python = ">=3.8" +dependencies = [ + "httpx>=0.25.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "pytest-mock>=3.10.0", + "black>=23.0.0", + "isort>=5.12.0", + "flake8>=6.0.0", + "mypy>=1.5.0", + "pre-commit>=3.0.0", + "httpx[cli]>=0.25.0" +] +docs = [ + "mkdocs>=1.5.0", + "mkdocs-material>=9.0.0" +] + +[project.urls] +Homepage = "https://github.com/ambient-code/platform" +Documentation = "https://docs.ambient-code.io" +Repository = "https://github.com/ambient-code/platform" +Issues = "https://github.com/ambient-code/platform/issues" +Changelog = "https://github.com/ambient-code/platform/releases" + +[tool.setuptools.packages.find] +where = ["."] +include = ["ambient_platform*"] + +[tool.black] +line-length = 88 +target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["ambient_platform"] + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +addopts = [ + "-ra", + "--strict-markers", + "--strict-config" +] +markers = [ + "integration: marks tests as integration tests (deselect with '-m \"not integration\"')", + "slow: marks tests as slow (deselect with '-m \"not slow\"')" +] \ No newline at end of file diff --git a/components/ambient-sdk/python-sdk/test.sh b/components/ambient-sdk/python-sdk/test.sh new file mode 100755 index 000000000..9cdbd9aab --- /dev/null +++ b/components/ambient-sdk/python-sdk/test.sh @@ -0,0 +1,231 @@ +#!/bin/bash + +# Ambient Platform Python SDK Test Script +# This script sets up the environment and runs the Python SDK example + +set -e # Exit on any error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Print colored output +print_error() { + echo -e "${RED}❌ $1${NC}" >&2 +} + +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +print_info() { + echo -e "${BLUE}ℹ $1${NC}" +} + +print_header() { + echo -e "${BLUE}🐍 Ambient Platform Python SDK Test${NC}" + echo -e "${BLUE}====================================${NC}" +} + +# Check required environment variables +check_environment() { + local missing_vars=() + + if [[ -z "${AMBIENT_TOKEN:-}" ]]; then + missing_vars+=("AMBIENT_TOKEN") + fi + + if [[ -z "${AMBIENT_PROJECT:-}" ]]; then + missing_vars+=("AMBIENT_PROJECT") + fi + + if [[ -z "${AMBIENT_API_URL:-}" ]]; then + missing_vars+=("AMBIENT_API_URL") + fi + + if [[ ${#missing_vars[@]} -gt 0 ]]; then + print_error "Missing required environment variables:" + echo + for var in "${missing_vars[@]}"; do + echo " - $var" + done + echo + print_info "Please set all required environment variables:" + echo + echo " export AMBIENT_TOKEN=\"your-bearer-token\"" + echo " export AMBIENT_PROJECT=\"your-project-name\"" + echo " export AMBIENT_API_URL=\"https://your-api-endpoint.com\"" + echo + print_info "Examples:" + echo + echo " # Using OpenShift token (recommended):" + echo " export AMBIENT_TOKEN=\"\$(oc whoami -t)\"" + echo " export AMBIENT_PROJECT=\"anynamespace\"" + echo " export AMBIENT_API_URL=\"https://public-api-route-yournamespace.apps.rosa.xezue-pjejw-oy9.ag90.p3.openshiftapps.com\"" + echo + echo " # Using manual token:" + echo " export AMBIENT_TOKEN=\"sha256~_3FClshuberfakepO_BGI_tZg_not_real_token_Jv72pRN-r5o\"" + echo " export AMBIENT_PROJECT=\"anynamespace\"" + echo " export AMBIENT_API_URL=\"https://public-api-route-yournamespace.apps.rosa.xezue-pjejw-oy9.ag90.p3.openshiftapps.com\"" + echo + print_warning "Then run this script again: ./test.sh" + exit 1 + fi + + print_success "All required environment variables are set" +} + +# Validate environment variables +validate_environment() { + print_info "Validating environment variables..." + + # Check token format (should not contain AMBIENT_TOKEN= prefix) + if [[ "${AMBIENT_TOKEN}" == *"AMBIENT_TOKEN="* ]]; then + print_error "Invalid token format detected" + echo + print_info "Your token contains 'AMBIENT_TOKEN=' which will cause API errors." + echo "Current token: ${AMBIENT_TOKEN}" + echo + print_info "Please fix your token by removing the duplicate prefix:" + echo "export AMBIENT_TOKEN=\"${AMBIENT_TOKEN#*AMBIENT_TOKEN=}\"" + exit 1 + fi + + # Check if URL is valid format + if [[ ! "${AMBIENT_API_URL}" =~ ^https?:// ]]; then + print_warning "API URL should start with http:// or https://" + print_info "Current URL: ${AMBIENT_API_URL}" + fi + + # Check project name format (basic validation) + if [[ ! "${AMBIENT_PROJECT}" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then + print_warning "Project name should follow Kubernetes naming conventions (lowercase alphanumeric with hyphens)" + print_info "Current project: ${AMBIENT_PROJECT}" + fi + + print_success "Environment variables validated" + + # Display configuration + echo + print_info "Configuration:" + echo " API URL: ${AMBIENT_API_URL}" + echo " Project: ${AMBIENT_PROJECT}" + echo " Token length: ${#AMBIENT_TOKEN} characters" + echo " Token prefix: ${AMBIENT_TOKEN:0:12}..." +} + +# Check if we're in the right directory +check_directory() { + if [[ ! -f "pyproject.toml" ]] || [[ ! -d "ambient_platform" ]] || [[ ! -f "examples/main.py" ]]; then + print_error "This script must be run from the python-sdk directory" + echo + print_info "Expected directory structure:" + echo " python-sdk/" + echo " ├── pyproject.toml" + echo " ├── ambient_platform/" + echo " └── examples/main.py" + echo + print_info "Please navigate to the correct directory:" + echo " cd /path/to/platform/components/ambient-sdk/python-sdk" + echo " ./test.sh" + exit 1 + fi + + print_success "Running from correct directory: $(pwd)" +} + +# Setup Python virtual environment +setup_venv() { + print_info "Setting up Python virtual environment..." + + if [[ ! -d "venv" ]]; then + print_info "Creating virtual environment..." + python -m venv venv + print_success "Virtual environment created" + else + print_success "Virtual environment already exists" + fi +} + +# Install dependencies +install_dependencies() { + print_info "Installing dependencies..." + + # Activate virtual environment + source venv/bin/activate + + # Install SDK in development mode + pip install -e . > /dev/null 2>&1 + + print_success "Dependencies installed successfully" +} + +# Test SDK import +test_import() { + print_info "Testing SDK import..." + + # Activate virtual environment + source venv/bin/activate + + # Test basic import + python -c "import ambient_platform; print('Import successful')" > /dev/null + + # Test specific imports + python -c " +from ambient_platform import ( + AmbientClient, + CreateSessionRequest, + RepoHTTP, + StatusPending, + StatusCompleted +) +print('All imports successful') +" > /dev/null + + print_success "SDK imports working correctly" +} + +# Run the example +run_example() { + print_info "Running Python SDK example..." + echo + + # Activate virtual environment and run example + source venv/bin/activate + python examples/main.py +} + +# Main execution +main() { + print_header + echo + + # Run all checks and setup + check_directory + check_environment + validate_environment + echo + + setup_venv + install_dependencies + test_import + echo + + print_success "Setup complete! Running example..." + echo + + run_example + + echo + print_success "Python SDK test completed successfully!" +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/components/ambient-sdk/python-sdk/tests/__init__.py b/components/ambient-sdk/python-sdk/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/components/ambient-sdk/python-sdk/tests/test_client.py b/components/ambient-sdk/python-sdk/tests/test_client.py new file mode 100644 index 000000000..af41961d5 --- /dev/null +++ b/components/ambient-sdk/python-sdk/tests/test_client.py @@ -0,0 +1,197 @@ +import os + +import pytest + +from ambient_platform.client import AmbientClient + + +class TestClientValidation: + def test_empty_token_raises(self): + with pytest.raises(ValueError, match="token cannot be empty"): + AmbientClient(base_url="http://localhost:8080", token="", project="test") + + def test_short_token_raises(self): + with pytest.raises(ValueError, match="too short"): + AmbientClient(base_url="http://localhost:8080", token="abc", project="test") + + def test_placeholder_token_raises(self): + with pytest.raises(ValueError, match="placeholder"): + AmbientClient( + base_url="http://localhost:8080", + token="YOUR_TOKEN_HERE", + project="test", + ) + + def test_empty_project_raises(self): + with pytest.raises(ValueError, match="project cannot be empty"): + AmbientClient( + base_url="http://localhost:8080", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="", + ) + + def test_invalid_project_chars_raises(self): + with pytest.raises(ValueError, match="alphanumeric"): + AmbientClient( + base_url="http://localhost:8080", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="bad project!", + ) + + def test_long_project_raises(self): + with pytest.raises(ValueError, match="63 characters"): + AmbientClient( + base_url="http://localhost:8080", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="a" * 64, + ) + + def test_empty_base_url_raises(self): + with pytest.raises(ValueError, match="base URL cannot be empty"): + AmbientClient( + base_url="", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="test", + ) + + def test_invalid_url_scheme_raises(self): + with pytest.raises(ValueError, match="scheme must be http or https"): + AmbientClient( + base_url="ftp://api.test.com", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="test", + ) + + def test_placeholder_domain_raises(self): + with pytest.raises(ValueError, match="placeholder domain"): + AmbientClient( + base_url="https://example.com", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="test", + ) + + def test_valid_client_creation(self): + client = AmbientClient( + base_url="https://api.real-platform.com", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="my-project", + ) + assert client._base_url == "https://api.real-platform.com" + assert client._project == "my-project" + assert client._base_path == "/api/ambient-api-server/v1" + client.close() + + def test_trailing_slash_stripped(self): + client = AmbientClient( + base_url="https://api.real-platform.com/", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="my-project", + ) + assert client._base_url == "https://api.real-platform.com" + client.close() + + def test_context_manager(self): + with AmbientClient( + base_url="https://api.real-platform.com", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="my-project", + ) as client: + assert client._project == "my-project" + + +class TestClientFromEnv: + def test_missing_token_raises(self, monkeypatch): + monkeypatch.delenv("AMBIENT_TOKEN", raising=False) + monkeypatch.delenv("AMBIENT_PROJECT", raising=False) + with pytest.raises(ValueError, match="AMBIENT_TOKEN"): + AmbientClient.from_env() + + def test_missing_project_raises(self, monkeypatch): + monkeypatch.setenv("AMBIENT_TOKEN", "sha256~abcdefghijklmnopqrstuvwxyz1234567890") + monkeypatch.delenv("AMBIENT_PROJECT", raising=False) + with pytest.raises(ValueError, match="AMBIENT_PROJECT"): + AmbientClient.from_env() + + def test_defaults_url(self, monkeypatch): + monkeypatch.setenv("AMBIENT_TOKEN", "sha256~abcdefghijklmnopqrstuvwxyz1234567890") + monkeypatch.setenv("AMBIENT_PROJECT", "test-project") + monkeypatch.delenv("AMBIENT_API_URL", raising=False) + client = AmbientClient.from_env() + assert client._base_url == "http://localhost:8080" + client.close() + + def test_custom_url(self, monkeypatch): + monkeypatch.setenv("AMBIENT_TOKEN", "sha256~abcdefghijklmnopqrstuvwxyz1234567890") + monkeypatch.setenv("AMBIENT_PROJECT", "test-project") + monkeypatch.setenv("AMBIENT_API_URL", "https://custom.api.com") + client = AmbientClient.from_env() + assert client._base_url == "https://custom.api.com" + client.close() + + +class TestClientResourceAccessors: + def setup_method(self): + self.client = AmbientClient( + base_url="https://api.real-platform.com", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="test-project", + ) + + def teardown_method(self): + self.client.close() + + def test_sessions_accessor(self): + api = self.client.sessions + assert api is not None + assert self.client.sessions is api + + def test_projects_accessor(self): + assert self.client.projects is not None + + def test_project_settings_accessor(self): + assert self.client.project_settings is not None + + def test_users_accessor(self): + assert self.client.users is not None + + def test_api_caching(self): + api1 = self.client.sessions + api2 = self.client.sessions + assert api1 is api2 + + +class TestSessionAPIWP6Methods: + def setup_method(self): + self.client = AmbientClient( + base_url="https://api.real-platform.com", + token="sha256~abcdefghijklmnopqrstuvwxyz1234567890", + project="test-project", + ) + + def teardown_method(self): + self.client.close() + + def test_has_start_method(self): + assert hasattr(self.client.sessions, "start") + assert callable(self.client.sessions.start) + + def test_has_stop_method(self): + assert hasattr(self.client.sessions, "stop") + assert callable(self.client.sessions.stop) + + def test_has_update_status_method(self): + assert hasattr(self.client.sessions, "update_status") + assert callable(self.client.sessions.update_status) + + def test_has_all_crud_and_action_methods(self): + api = self.client.sessions + expected = ["create", "get", "list", "update", "update_status", "start", "stop", "list_all"] + for method_name in expected: + assert hasattr(api, method_name), f"SessionAPI missing method: {method_name}" + assert callable(getattr(api, method_name)), f"SessionAPI.{method_name} is not callable" + + def test_non_session_api_has_no_actions(self): + api = self.client.users + assert not hasattr(api, "start") + assert not hasattr(api, "stop") + assert not hasattr(api, "update_status") diff --git a/components/ambient-sdk/python-sdk/tests/test_types.py b/components/ambient-sdk/python-sdk/tests/test_types.py new file mode 100644 index 000000000..c2d1fe396 --- /dev/null +++ b/components/ambient-sdk/python-sdk/tests/test_types.py @@ -0,0 +1,486 @@ +import pytest +from datetime import datetime, timezone + +from ambient_platform import ( + Session, + SessionPatch, + SessionStatusPatch, + User, + Project, + ProjectPatch, + ProjectSettings, + ProjectSettingsPatch, + UserPatch, +) +from ambient_platform.session import SessionBuilder, SessionList +from ambient_platform.project import ProjectBuilder, ProjectList +from ambient_platform.project_settings import ProjectSettingsBuilder, ProjectSettingsList +from ambient_platform.user import UserBuilder, UserList +from ambient_platform._base import ListOptions, APIError, ObjectReference, _parse_datetime + + +class TestSessionBuilder: + def test_valid_session(self): + data = ( + Session.builder() + .name("test-session") + .prompt("analyze this") + .repo_url("https://github.com/foo/bar") + .workflow_id("wf-123") + .assigned_user_id("user-1") + .build() + ) + assert data["name"] == "test-session" + assert data["prompt"] == "analyze this" + assert data["repo_url"] == "https://github.com/foo/bar" + assert data["workflow_id"] == "wf-123" + + def test_missing_name_raises(self): + with pytest.raises(ValueError, match="name is required"): + Session.builder().prompt("test").build() + + +class TestSessionBuilderFields: + def test_all_writable_fields(self): + data = ( + Session.builder() + .name("full-session") + .prompt("test prompt") + .llm_model("claude-4-opus") + .llm_temperature(0.7) + .llm_max_tokens(4096) + .repos('[{"url":"https://github.com/org/repo"}]') + .labels("env=dev,team=platform") + .annotations("note=test") + .project_id("proj-1") + .parent_session_id("parent-123") + .bot_account_name("bot-1") + .resource_overrides('{"cpu":"2","memory":"4Gi"}') + .environment_variables('{"DEBUG":"true"}') + .timeout(3600) + .build() + ) + assert data["llm_temperature"] == 0.7 + assert data["llm_max_tokens"] == 4096 + assert data["llm_model"] == "claude-4-opus" + assert data["timeout"] == 3600 + assert data["project_id"] == "proj-1" + assert data["bot_account_name"] == "bot-1" + + def test_readonly_fields_not_on_builder(self): + builder = Session.builder() + for readonly_field in [ + "phase", + "kube_cr_name", + "kube_cr_uid", + "kube_namespace", + "completion_time", + "start_time", + "sdk_restart_count", + "sdk_session_id", + "conditions", + "reconciled_repos", + "reconciled_workflow", + ]: + assert not hasattr(builder, readonly_field), ( + f"Builder should NOT have readOnly method: {readonly_field}" + ) + + def test_writable_fields_present_on_builder(self): + builder = Session.builder() + for writable_field in [ + "name", + "prompt", + "llm_model", + "llm_temperature", + "llm_max_tokens", + "repos", + "labels", + "annotations", + "project_id", + "parent_session_id", + "bot_account_name", + "resource_overrides", + "environment_variables", + "timeout", + "workflow_id", + "repo_url", + "assigned_user_id", + ]: + assert hasattr(builder, writable_field), ( + f"Builder should have writable method: {writable_field}" + ) + + +class TestSessionFromDict: + def test_float_field(self): + s = Session.from_dict({"name": "t", "llm_temperature": 0.85}) + assert s.llm_temperature == 0.85 + + def test_float_default(self): + s = Session.from_dict({"name": "t"}) + assert s.llm_temperature == 0.0 + + def test_int_field(self): + s = Session.from_dict({"name": "t", "llm_max_tokens": 8192}) + assert s.llm_max_tokens == 8192 + + def test_readonly_fields_deserialized(self): + s = Session.from_dict({ + "name": "t", + "phase": "running", + "kube_cr_name": "cr-123", + "kube_cr_uid": "uid-456", + "kube_namespace": "ambient-code", + "conditions": "Ready", + "reconciled_repos": '["repo1"]', + "reconciled_workflow": "wf-done", + "sdk_restart_count": 3, + "sdk_session_id": "sdk-xyz", + "start_time": "2026-01-15T10:00:00Z", + "completion_time": "2026-01-15T11:00:00Z", + }) + assert s.phase == "running" + assert s.kube_cr_name == "cr-123" + assert s.kube_namespace == "ambient-code" + assert s.sdk_restart_count == 3 + assert s.start_time is not None + assert s.completion_time is not None + + def test_full_session(self): + s = Session.from_dict({ + "id": "sess-1", + "kind": "Session", + "name": "full-session", + "prompt": "analyze code", + "llm_model": "claude-4-opus", + "llm_temperature": 0.7, + "llm_max_tokens": 4096, + "timeout": 3600, + "project_id": "proj-1", + "phase": "completed", + "labels": "env=dev", + "repos": '[{"url":"repo"}]', + "bot_account_name": "bot-1", + "parent_session_id": "parent-sess", + }) + assert s.llm_temperature == 0.7 + assert s.phase == "completed" + assert s.bot_account_name == "bot-1" + + +class TestSessionPatch: + def test_patch_fields(self): + patch = ( + SessionPatch() + .llm_temperature(0.9) + .llm_max_tokens(8192) + .timeout(7200) + ) + data = patch.to_dict() + assert data["llm_temperature"] == 0.9 + assert data["llm_max_tokens"] == 8192 + assert data["timeout"] == 7200 + + def test_patch_readonly_fields_not_present(self): + patch = SessionPatch() + for readonly_field in [ + "phase", + "kube_cr_name", + "kube_cr_uid", + "kube_namespace", + "completion_time", + "start_time", + "sdk_restart_count", + "sdk_session_id", + "conditions", + "reconciled_repos", + "reconciled_workflow", + ]: + assert not hasattr(patch, readonly_field), ( + f"Patch should NOT have readOnly method: {readonly_field}" + ) + + def test_sets_only_specified_fields(self): + patch = SessionPatch().prompt("updated prompt") + data = patch.to_dict() + assert data == {"prompt": "updated prompt"} + assert "name" not in data + + +class TestSessionStatusPatch: + def test_all_fields(self): + patch = ( + SessionStatusPatch() + .phase("Running") + .sdk_session_id("sdk-123") + .sdk_restart_count(2) + .conditions('[{"type":"Ready","status":"True"}]') + .kube_cr_uid("uid-abc") + .kube_namespace("ambient-code") + .reconciled_repos('["repo1","repo2"]') + .reconciled_workflow('{"id":"wf-1"}') + ) + data = patch.to_dict() + assert data["phase"] == "Running" + assert data["sdk_restart_count"] == 2 + assert data["kube_cr_uid"] == "uid-abc" + assert data["kube_namespace"] == "ambient-code" + assert len(data) == 8 + + def test_sparse_update(self): + patch = SessionStatusPatch().phase("Completed") + data = patch.to_dict() + assert data == {"phase": "Completed"} + assert "kube_namespace" not in data + + def test_datetime_fields(self): + now = datetime.now(tz=timezone.utc) + patch = SessionStatusPatch().start_time(now).completion_time(now) + data = patch.to_dict() + assert data["start_time"] == now + assert data["completion_time"] == now + + def test_has_all_10_methods(self): + patch = SessionStatusPatch() + expected_methods = [ + "phase", + "start_time", + "completion_time", + "sdk_session_id", + "sdk_restart_count", + "conditions", + "reconciled_repos", + "reconciled_workflow", + "kube_cr_uid", + "kube_namespace", + ] + for method in expected_methods: + assert hasattr(patch, method), f"Missing method: {method}" + + +class TestProjectBuilder: + def test_valid_project(self): + data = ( + Project.builder() + .name("my-project") + .display_name("My Project") + .description("A test project") + .labels("env=dev") + .annotations("note=test") + .status("active") + .build() + ) + assert data["name"] == "my-project" + assert data["display_name"] == "My Project" + assert data["description"] == "A test project" + assert data["labels"] == "env=dev" + assert data["status"] == "active" + + def test_missing_name_raises(self): + with pytest.raises(ValueError, match="name is required"): + Project.builder().description("no name").build() + + +class TestProjectSettingsBuilder: + def test_valid(self): + data = ( + ProjectSettings.builder() + .project_id("proj-123") + .group_access("admin,dev") + .repositories("repo1,repo2") + .build() + ) + assert data["project_id"] == "proj-123" + assert data["group_access"] == "admin,dev" + + def test_missing_project_id_raises(self): + with pytest.raises(ValueError, match="project_id is required"): + ProjectSettings.builder().group_access("admin").build() + + +class TestUserBuilder: + def test_valid(self): + data = User.builder().name("Alice").username("alice").build() + assert data["name"] == "Alice" + assert data["username"] == "alice" + + def test_missing_name_raises(self): + with pytest.raises(ValueError, match="name is required"): + User.builder().username("alice").build() + + def test_missing_username_raises(self): + with pytest.raises(ValueError, match="username is required"): + User.builder().name("Alice").build() + + +class TestListOptions: + def test_defaults(self): + opts = ListOptions() + params = opts.to_params() + assert params["page"] == 1 + assert params["size"] == 100 + + def test_max_size_capped(self): + opts = ListOptions().size(999999) + params = opts.to_params() + assert params["size"] == 65500 + + def test_all_fields(self): + opts = ( + ListOptions() + .page(3) + .size(50) + .search("name like 'test%'") + .order_by("created_at desc") + .fields("id,name,status") + ) + params = opts.to_params() + assert params["page"] == 3 + assert params["size"] == 50 + assert params["search"] == "name like 'test%'" + assert params["orderBy"] == "created_at desc" + assert params["fields"] == "id,name,status" + + +class TestPatchBuilder: + def test_project_patch_all_fields(self): + patch = ( + ProjectPatch() + .name("renamed") + .display_name("Renamed") + .description("new desc") + .labels("env=prod") + .annotations("a=b") + .status("archived") + ) + data = patch.to_dict() + assert len(data) == 6 + + +class TestAPIError: + def test_str_format(self): + err = APIError(status_code=404, code="NOT_FOUND", reason="session not found") + assert str(err) == "ambient API error 404: NOT_FOUND — session not found" + + def test_from_dict(self): + err = APIError.from_dict( + {"code": "VALIDATION", "reason": "bad input", "operation_id": "op-1"}, + status_code=400, + ) + assert err.status_code == 400 + assert err.code == "VALIDATION" + assert err.operation_id == "op-1" + + def test_is_exception(self): + err = APIError(status_code=500, code="INTERNAL", reason="boom") + assert isinstance(err, Exception) + + +class TestFromDict: + def test_session_from_dict(self): + data = { + "id": "sess-123", + "kind": "Session", + "name": "test-session", + "prompt": "analyze", + "created_at": "2026-01-15T10:00:00Z", + } + s = Session.from_dict(data) + assert s.id == "sess-123" + assert s.name == "test-session" + assert s.prompt == "analyze" + assert s.created_at is not None + assert s.created_at.year == 2026 + + def test_project_from_dict(self): + data = { + "id": "proj-1", + "kind": "Project", + "name": "my-project", + "display_name": "My Project", + "status": "active", + } + p = Project.from_dict(data) + assert p.display_name == "My Project" + assert p.status == "active" + + def test_project_settings_from_dict(self): + data = { + "id": "ps-1", + "project_id": "proj-1", + "group_access": "admin", + } + ps = ProjectSettings.from_dict(data) + assert ps.project_id == "proj-1" + assert ps.group_access == "admin" + + def test_session_list_from_dict(self): + data = { + "kind": "SessionList", + "page": 1, + "size": 100, + "total": 2, + "items": [ + {"id": "s1", "name": "a"}, + {"id": "s2", "name": "b"}, + ], + } + sl = SessionList.from_dict(data) + assert sl.total == 2 + assert sl.page == 1 + assert len(sl.items) == 2 + assert sl.items[0].name == "a" + + def test_user_from_dict(self): + data = { + "id": "user-1", + "kind": "User", + "name": "Alice", + "username": "alice", + "email": "alice@example.com", + } + u = User.from_dict(data) + assert u.name == "Alice" + assert u.username == "alice" + assert u.email == "alice@example.com" + + +class TestParseDatetime: + def test_iso_with_z(self): + dt = _parse_datetime("2026-01-15T10:00:00Z") + assert dt is not None + assert dt.year == 2026 + + def test_iso_with_offset(self): + dt = _parse_datetime("2026-01-15T10:00:00+00:00") + assert dt is not None + + def test_none(self): + assert _parse_datetime(None) is None + + def test_invalid_string(self): + assert _parse_datetime("not-a-date") is None + + def test_datetime_passthrough(self): + now = datetime.now(tz=timezone.utc) + assert _parse_datetime(now) is now + + +class TestObjectReference: + def test_from_dict(self): + data = { + "id": "ref-1", + "kind": "Session", + "href": "/v1/sessions/ref-1", + "created_at": "2026-01-15T10:00:00Z", + } + ref = ObjectReference.from_dict(data) + assert ref.id == "ref-1" + assert ref.kind == "Session" + assert ref.created_at is not None + + def test_frozen(self): + ref = ObjectReference(id="test") + with pytest.raises(AttributeError): + ref.id = "changed" diff --git a/components/ambient-sdk/ts-sdk/jest.config.js b/components/ambient-sdk/ts-sdk/jest.config.js new file mode 100644 index 000000000..847831cbd --- /dev/null +++ b/components/ambient-sdk/ts-sdk/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + moduleFileExtensions: ['ts', 'js'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, +}; diff --git a/components/ambient-sdk/ts-sdk/package-lock.json b/components/ambient-sdk/ts-sdk/package-lock.json new file mode 100644 index 000000000..8f7fe05ce --- /dev/null +++ b/components/ambient-sdk/ts-sdk/package-lock.json @@ -0,0 +1,3847 @@ +{ + "name": "@ambient-platform/sdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ambient-platform/sdk", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^20.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.0", + "typescript": "~5.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/components/ambient-sdk/ts-sdk/package.json b/components/ambient-sdk/ts-sdk/package.json new file mode 100644 index 000000000..c23e7da8d --- /dev/null +++ b/components/ambient-sdk/ts-sdk/package.json @@ -0,0 +1,33 @@ +{ + "name": "@ambient-platform/sdk", + "version": "0.1.0", + "description": "TypeScript client library for the Ambient Code Platform REST API", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "node --experimental-vm-modules node_modules/.bin/jest", + "clean": "rm -rf dist" + }, + "keywords": [ + "ambient", + "sdk", + "api-client" + ], + "license": "Apache-2.0", + "devDependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^20.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.0", + "typescript": "~5.5.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/components/ambient-sdk/ts-sdk/src/base.ts b/components/ambient-sdk/ts-sdk/src/base.ts new file mode 100644 index 000000000..acbc73a62 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/base.ts @@ -0,0 +1,141 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +export type ObjectReference = { + id: string; + kind: string; + href: string; + created_at: string | null; + updated_at: string | null; +}; + +export type ListMeta = { + kind: string; + page: number; + size: number; + total: number; +}; + +export type APIError = { + id: string; + kind: string; + href: string; + code: string; + reason: string; + operation_id: string; + status_code: number; +}; + +export class AmbientAPIError extends Error { + readonly statusCode: number; + readonly code: string; + readonly reason: string; + readonly operationId: string; + + constructor(error: APIError) { + super(`ambient API error ${error.status_code}: ${error.code} — ${error.reason}`); + this.name = 'AmbientAPIError'; + this.statusCode = error.status_code; + this.code = error.code; + this.reason = error.reason; + this.operationId = error.operation_id; + } +} + +export type ListOptions = { + page?: number; + size?: number; + search?: string; + orderBy?: string; + fields?: string; +}; + +export function buildQueryString(opts?: ListOptions): string { + if (!opts) return ''; + const params = new URLSearchParams(); + if (opts.page !== undefined) params.set('page', String(opts.page)); + if (opts.size !== undefined) params.set('size', String(Math.min(opts.size, 65500))); + if (opts.search) params.set('search', opts.search); + if (opts.orderBy) params.set('orderBy', opts.orderBy); + if (opts.fields) params.set('fields', opts.fields); + const qs = params.toString(); + return qs ? `?${qs}` : ''; +} + +export type RequestOptions = { + signal?: AbortSignal; +}; + +export type AmbientClientConfig = { + baseUrl: string; + token: string; + project: string; +}; + +export async function ambientFetch( + config: AmbientClientConfig, + method: string, + path: string, + body?: unknown, + requestOpts?: RequestOptions, +): Promise { + const url = `${config.baseUrl}/api/ambient-api-server/v1${path}`; + const headers: Record = { + 'Authorization': `Bearer ${config.token}`, + 'X-Ambient-Project': config.project, + }; + if (body !== undefined) { + headers['Content-Type'] = 'application/json'; + } + + const resp = await fetch(url, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + signal: requestOpts?.signal, + }); + + if (!resp.ok) { + let errorData: APIError; + try { + const jsonData = await resp.json(); + // Validate that this looks like an APIError response + if (typeof jsonData === 'object' && jsonData !== null) { + errorData = { + id: typeof jsonData.id === 'string' ? jsonData.id : '', + kind: typeof jsonData.kind === 'string' ? jsonData.kind : 'Error', + href: typeof jsonData.href === 'string' ? jsonData.href : '', + code: typeof jsonData.code === 'string' ? jsonData.code : 'unknown_error', + reason: typeof jsonData.reason === 'string' ? jsonData.reason : `HTTP ${resp.status}: ${resp.statusText}`, + operation_id: typeof jsonData.operation_id === 'string' ? jsonData.operation_id : '', + status_code: resp.status, + }; + } else { + throw new Error('Invalid error response format'); + } + } catch { + errorData = { + id: '', + kind: 'Error', + href: '', + code: 'unknown_error', + reason: `HTTP ${resp.status}: ${resp.statusText}`, + operation_id: '', + status_code: resp.status, + }; + } + throw new AmbientAPIError(errorData); + } + + if (resp.status === 204) { + return undefined as T; + } + + // Parse JSON response with validation + const jsonData = await resp.json(); + // Note: In a production system, you might want to add runtime type validation here + // For now, we trust the API contract but avoid the unsafe 'as Promise' cast + return jsonData; +} diff --git a/components/ambient-sdk/ts-sdk/src/client.ts b/components/ambient-sdk/ts-sdk/src/client.ts new file mode 100644 index 000000000..1c1e3dfb3 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/client.ts @@ -0,0 +1,71 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { AmbientClientConfig } from './base'; +import { ProjectAPI } from './project_api'; +import { ProjectSettingsAPI } from './project_settings_api'; +import { SessionAPI } from './session_api'; +import { UserAPI } from './user_api'; + + +export class AmbientClient { + private readonly config: AmbientClientConfig; + + readonly projects: ProjectAPI; + readonly projectSettings: ProjectSettingsAPI; + readonly sessions: SessionAPI; + readonly users: UserAPI; + + constructor(config: AmbientClientConfig) { + if (!config.baseUrl) { + throw new Error('baseUrl is required'); + } + if (!config.token) { + throw new Error('token is required'); + } + if (config.token.length < 20) { + throw new Error('token is too short (minimum 20 characters)'); + } + if (config.token === 'YOUR_TOKEN_HERE' || config.token === 'PLACEHOLDER_TOKEN') { + throw new Error('placeholder token is not allowed'); + } + if (!config.project) { + throw new Error('project is required'); + } + if (config.project.length > 63) { + throw new Error('project name cannot exceed 63 characters'); + } + + const url = new URL(config.baseUrl); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error('only HTTP and HTTPS schemes are supported'); + } + + this.config = { + ...config, + baseUrl: config.baseUrl.replace(/\/+$/, ''), + }; + + this.projects = new ProjectAPI(this.config); + this.projectSettings = new ProjectSettingsAPI(this.config); + this.sessions = new SessionAPI(this.config); + this.users = new UserAPI(this.config); + } + + static fromEnv(): AmbientClient { + const baseUrl = process.env.AMBIENT_API_URL || 'http://localhost:8080'; + const token = process.env.AMBIENT_TOKEN; + const project = process.env.AMBIENT_PROJECT; + + if (!token) { + throw new Error('AMBIENT_TOKEN environment variable is required'); + } + if (!project) { + throw new Error('AMBIENT_PROJECT environment variable is required'); + } + + return new AmbientClient({ baseUrl, token, project }); + } +} diff --git a/components/ambient-sdk/ts-sdk/src/index.ts b/components/ambient-sdk/ts-sdk/src/index.ts new file mode 100644 index 000000000..8b50d9ff2 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/index.ts @@ -0,0 +1,25 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +export { AmbientClient } from './client'; +export type { AmbientClientConfig, ListOptions, RequestOptions, ObjectReference, ListMeta, APIError } from './base'; +export { AmbientAPIError, buildQueryString } from './base'; + +export type { Project, ProjectList, ProjectCreateRequest, ProjectPatchRequest } from './project'; +export { ProjectBuilder, ProjectPatchBuilder } from './project'; +export { ProjectAPI } from './project_api'; + +export type { ProjectSettings, ProjectSettingsList, ProjectSettingsCreateRequest, ProjectSettingsPatchRequest } from './project_settings'; +export { ProjectSettingsBuilder, ProjectSettingsPatchBuilder } from './project_settings'; +export { ProjectSettingsAPI } from './project_settings_api'; + +export type { Session, SessionList, SessionCreateRequest, SessionPatchRequest, SessionStatusPatchRequest } from './session'; +export { SessionBuilder, SessionPatchBuilder, SessionStatusPatchBuilder } from './session'; +export { SessionAPI } from './session_api'; + +export type { User, UserList, UserCreateRequest, UserPatchRequest } from './user'; +export { UserBuilder, UserPatchBuilder } from './user'; +export { UserAPI } from './user_api'; + diff --git a/components/ambient-sdk/ts-sdk/src/project.ts b/components/ambient-sdk/ts-sdk/src/project.ts new file mode 100644 index 000000000..6e1c5ad82 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/project.ts @@ -0,0 +1,119 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { ObjectReference, ListMeta } from './base'; + +export type Project = ObjectReference & { + annotations: string; + description: string; + display_name: string; + labels: string; + name: string; + status: string; +}; + +export type ProjectList = ListMeta & { + items: Project[]; +}; + +export type ProjectCreateRequest = { + annotations?: string; + description?: string; + display_name?: string; + labels?: string; + name: string; + status?: string; +}; + +export type ProjectPatchRequest = { + annotations?: string; + description?: string; + display_name?: string; + labels?: string; + name?: string; + status?: string; +}; + +export class ProjectBuilder { + private data: Record = {}; + + + annotations(value: string): this { + this.data['annotations'] = value; + return this; + } + + description(value: string): this { + this.data['description'] = value; + return this; + } + + displayName(value: string): this { + this.data['display_name'] = value; + return this; + } + + labels(value: string): this { + this.data['labels'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + status(value: string): this { + this.data['status'] = value; + return this; + } + + build(): ProjectCreateRequest { + if (!this.data['name']) { + throw new Error('name is required'); + } + return this.data as ProjectCreateRequest; + } +} + +export class ProjectPatchBuilder { + private data: Record = {}; + + + annotations(value: string): this { + this.data['annotations'] = value; + return this; + } + + description(value: string): this { + this.data['description'] = value; + return this; + } + + displayName(value: string): this { + this.data['display_name'] = value; + return this; + } + + labels(value: string): this { + this.data['labels'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + status(value: string): this { + this.data['status'] = value; + return this; + } + + build(): ProjectPatchRequest { + return this.data as ProjectPatchRequest; + } +} + diff --git a/components/ambient-sdk/ts-sdk/src/project_api.ts b/components/ambient-sdk/ts-sdk/src/project_api.ts new file mode 100644 index 000000000..ef60885be --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/project_api.ts @@ -0,0 +1,46 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { AmbientClientConfig, ListOptions, RequestOptions } from './base'; +import { ambientFetch, buildQueryString } from './base'; +import type { Project, ProjectList, ProjectCreateRequest, ProjectPatchRequest } from './project'; + +export class ProjectAPI { + constructor(private readonly config: AmbientClientConfig) {} + + async create(data: ProjectCreateRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'POST', '/projects', data, opts); + } + + async get(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'GET', `/projects/${id}`, undefined, opts); + } + + async list(listOpts?: ListOptions, opts?: RequestOptions): Promise { + const qs = buildQueryString(listOpts); + return ambientFetch(this.config, 'GET', `/projects${qs}`, undefined, opts); + } + async update(id: string, patch: ProjectPatchRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'PATCH', `/projects/${id}`, patch, opts); + } + + async delete(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'DELETE', `/projects/${id}`, undefined, opts); + } + + async *listAll(size: number = 100, opts?: RequestOptions): AsyncGenerator { + let page = 1; + while (true) { + const result = await this.list({ page, size }, opts); + for (const item of result.items) { + yield item; + } + if (page * size >= result.total) { + break; + } + page++; + } + } +} diff --git a/components/ambient-sdk/ts-sdk/src/project_settings.ts b/components/ambient-sdk/ts-sdk/src/project_settings.ts new file mode 100644 index 000000000..da2283851 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/project_settings.ts @@ -0,0 +1,80 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { ObjectReference, ListMeta } from './base'; + +export type ProjectSettings = ObjectReference & { + group_access: string; + project_id: string; + repositories: string; +}; + +export type ProjectSettingsList = ListMeta & { + items: ProjectSettings[]; +}; + +export type ProjectSettingsCreateRequest = { + group_access?: string; + project_id: string; + repositories?: string; +}; + +export type ProjectSettingsPatchRequest = { + group_access?: string; + project_id?: string; + repositories?: string; +}; + +export class ProjectSettingsBuilder { + private data: Record = {}; + + + groupAccess(value: string): this { + this.data['group_access'] = value; + return this; + } + + projectId(value: string): this { + this.data['project_id'] = value; + return this; + } + + repositories(value: string): this { + this.data['repositories'] = value; + return this; + } + + build(): ProjectSettingsCreateRequest { + if (!this.data['project_id']) { + throw new Error('project_id is required'); + } + return this.data as ProjectSettingsCreateRequest; + } +} + +export class ProjectSettingsPatchBuilder { + private data: Record = {}; + + + groupAccess(value: string): this { + this.data['group_access'] = value; + return this; + } + + projectId(value: string): this { + this.data['project_id'] = value; + return this; + } + + repositories(value: string): this { + this.data['repositories'] = value; + return this; + } + + build(): ProjectSettingsPatchRequest { + return this.data as ProjectSettingsPatchRequest; + } +} + diff --git a/components/ambient-sdk/ts-sdk/src/project_settings_api.ts b/components/ambient-sdk/ts-sdk/src/project_settings_api.ts new file mode 100644 index 000000000..d7dd8e599 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/project_settings_api.ts @@ -0,0 +1,46 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { AmbientClientConfig, ListOptions, RequestOptions } from './base'; +import { ambientFetch, buildQueryString } from './base'; +import type { ProjectSettings, ProjectSettingsList, ProjectSettingsCreateRequest, ProjectSettingsPatchRequest } from './project_settings'; + +export class ProjectSettingsAPI { + constructor(private readonly config: AmbientClientConfig) {} + + async create(data: ProjectSettingsCreateRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'POST', '/project_settings', data, opts); + } + + async get(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'GET', `/project_settings/${id}`, undefined, opts); + } + + async list(listOpts?: ListOptions, opts?: RequestOptions): Promise { + const qs = buildQueryString(listOpts); + return ambientFetch(this.config, 'GET', `/project_settings${qs}`, undefined, opts); + } + async update(id: string, patch: ProjectSettingsPatchRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'PATCH', `/project_settings/${id}`, patch, opts); + } + + async delete(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'DELETE', `/project_settings/${id}`, undefined, opts); + } + + async *listAll(size: number = 100, opts?: RequestOptions): AsyncGenerator { + let page = 1; + while (true) { + const result = await this.list({ page, size }, opts); + for (const item of result.items) { + yield item; + } + if (page * size >= result.total) { + break; + } + page++; + } + } +} diff --git a/components/ambient-sdk/ts-sdk/src/session.ts b/components/ambient-sdk/ts-sdk/src/session.ts new file mode 100644 index 000000000..d80286e66 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/session.ts @@ -0,0 +1,340 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { ObjectReference, ListMeta } from './base'; + +export type Session = ObjectReference & { + annotations: string; + assigned_user_id: string; + bot_account_name: string; + completion_time: string; + conditions: string; + created_by_user_id: string; + environment_variables: string; + kube_cr_name: string; + kube_cr_uid: string; + kube_namespace: string; + labels: string; + llm_max_tokens: number; + llm_model: string; + llm_temperature: number; + name: string; + parent_session_id: string; + phase: string; + project_id: string; + prompt: string; + reconciled_repos: string; + reconciled_workflow: string; + repo_url: string; + repos: string; + resource_overrides: string; + sdk_restart_count: number; + sdk_session_id: string; + start_time: string; + timeout: number; + workflow_id: string; +}; + +export type SessionList = ListMeta & { + items: Session[]; +}; + +export type SessionCreateRequest = { + annotations?: string; + assigned_user_id?: string; + bot_account_name?: string; + environment_variables?: string; + labels?: string; + llm_max_tokens?: number; + llm_model?: string; + llm_temperature?: number; + name: string; + parent_session_id?: string; + project_id?: string; + prompt?: string; + repo_url?: string; + repos?: string; + resource_overrides?: string; + timeout?: number; + workflow_id?: string; +}; + +export type SessionPatchRequest = { + annotations?: string; + assigned_user_id?: string; + bot_account_name?: string; + environment_variables?: string; + labels?: string; + llm_max_tokens?: number; + llm_model?: string; + llm_temperature?: number; + name?: string; + parent_session_id?: string; + prompt?: string; + repo_url?: string; + repos?: string; + resource_overrides?: string; + timeout?: number; + workflow_id?: string; +}; + +export type SessionStatusPatchRequest = { + completion_time?: string; + conditions?: string; + kube_cr_uid?: string; + kube_namespace?: string; + phase?: string; + reconciled_repos?: string; + reconciled_workflow?: string; + sdk_restart_count?: number; + sdk_session_id?: string; + start_time?: string; +}; + +export class SessionBuilder { + private data: Record = {}; + + + annotations(value: string): this { + this.data['annotations'] = value; + return this; + } + + assignedUserId(value: string): this { + this.data['assigned_user_id'] = value; + return this; + } + + botAccountName(value: string): this { + this.data['bot_account_name'] = value; + return this; + } + + environmentVariables(value: string): this { + this.data['environment_variables'] = value; + return this; + } + + labels(value: string): this { + this.data['labels'] = value; + return this; + } + + llmMaxTokens(value: number): this { + this.data['llm_max_tokens'] = value; + return this; + } + + llmModel(value: string): this { + this.data['llm_model'] = value; + return this; + } + + llmTemperature(value: number): this { + this.data['llm_temperature'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + parentSessionId(value: string): this { + this.data['parent_session_id'] = value; + return this; + } + + projectId(value: string): this { + this.data['project_id'] = value; + return this; + } + + prompt(value: string): this { + this.data['prompt'] = value; + return this; + } + + repoUrl(value: string): this { + this.data['repo_url'] = value; + return this; + } + + repos(value: string): this { + this.data['repos'] = value; + return this; + } + + resourceOverrides(value: string): this { + this.data['resource_overrides'] = value; + return this; + } + + timeout(value: number): this { + this.data['timeout'] = value; + return this; + } + + workflowId(value: string): this { + this.data['workflow_id'] = value; + return this; + } + + build(): SessionCreateRequest { + if (!this.data['name']) { + throw new Error('name is required'); + } + return this.data as SessionCreateRequest; + } +} + +export class SessionPatchBuilder { + private data: Record = {}; + + + annotations(value: string): this { + this.data['annotations'] = value; + return this; + } + + assignedUserId(value: string): this { + this.data['assigned_user_id'] = value; + return this; + } + + botAccountName(value: string): this { + this.data['bot_account_name'] = value; + return this; + } + + environmentVariables(value: string): this { + this.data['environment_variables'] = value; + return this; + } + + labels(value: string): this { + this.data['labels'] = value; + return this; + } + + llmMaxTokens(value: number): this { + this.data['llm_max_tokens'] = value; + return this; + } + + llmModel(value: string): this { + this.data['llm_model'] = value; + return this; + } + + llmTemperature(value: number): this { + this.data['llm_temperature'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + parentSessionId(value: string): this { + this.data['parent_session_id'] = value; + return this; + } + + prompt(value: string): this { + this.data['prompt'] = value; + return this; + } + + repoUrl(value: string): this { + this.data['repo_url'] = value; + return this; + } + + repos(value: string): this { + this.data['repos'] = value; + return this; + } + + resourceOverrides(value: string): this { + this.data['resource_overrides'] = value; + return this; + } + + timeout(value: number): this { + this.data['timeout'] = value; + return this; + } + + workflowId(value: string): this { + this.data['workflow_id'] = value; + return this; + } + + build(): SessionPatchRequest { + return this.data as SessionPatchRequest; + } +} + +export class SessionStatusPatchBuilder { + private data: Record = {}; + + + completionTime(value: string): this { + this.data['completion_time'] = value; + return this; + } + + conditions(value: string): this { + this.data['conditions'] = value; + return this; + } + + kubeCrUid(value: string): this { + this.data['kube_cr_uid'] = value; + return this; + } + + kubeNamespace(value: string): this { + this.data['kube_namespace'] = value; + return this; + } + + phase(value: string): this { + this.data['phase'] = value; + return this; + } + + reconciledRepos(value: string): this { + this.data['reconciled_repos'] = value; + return this; + } + + reconciledWorkflow(value: string): this { + this.data['reconciled_workflow'] = value; + return this; + } + + sdkRestartCount(value: number): this { + this.data['sdk_restart_count'] = value; + return this; + } + + sdkSessionId(value: string): this { + this.data['sdk_session_id'] = value; + return this; + } + + startTime(value: string): this { + this.data['start_time'] = value; + return this; + } + + build(): SessionStatusPatchRequest { + return this.data as SessionStatusPatchRequest; + } +} + diff --git a/components/ambient-sdk/ts-sdk/src/session_api.ts b/components/ambient-sdk/ts-sdk/src/session_api.ts new file mode 100644 index 000000000..1c4bf6479 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/session_api.ts @@ -0,0 +1,54 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { AmbientClientConfig, ListOptions, RequestOptions } from './base'; +import { ambientFetch, buildQueryString } from './base'; +import type { Session, SessionList, SessionCreateRequest, SessionPatchRequest, SessionStatusPatchRequest } from './session'; + +export class SessionAPI { + constructor(private readonly config: AmbientClientConfig) {} + + async create(data: SessionCreateRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'POST', '/sessions', data, opts); + } + + async get(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'GET', `/sessions/${id}`, undefined, opts); + } + + async list(listOpts?: ListOptions, opts?: RequestOptions): Promise { + const qs = buildQueryString(listOpts); + return ambientFetch(this.config, 'GET', `/sessions${qs}`, undefined, opts); + } + async update(id: string, patch: SessionPatchRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'PATCH', `/sessions/${id}`, patch, opts); + } + + async updateStatus(id: string, patch: SessionStatusPatchRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'PATCH', `/sessions/${id}/status`, patch, opts); + } + + async start(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'POST', `/sessions/${id}/start`, undefined, opts); + } + + async stop(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'POST', `/sessions/${id}/stop`, undefined, opts); + } + + async *listAll(size: number = 100, opts?: RequestOptions): AsyncGenerator { + let page = 1; + while (true) { + const result = await this.list({ page, size }, opts); + for (const item of result.items) { + yield item; + } + if (page * size >= result.total) { + break; + } + page++; + } + } +} diff --git a/components/ambient-sdk/ts-sdk/src/user.ts b/components/ambient-sdk/ts-sdk/src/user.ts new file mode 100644 index 000000000..4716ad861 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/user.ts @@ -0,0 +1,83 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { ObjectReference, ListMeta } from './base'; + +export type User = ObjectReference & { + email: string; + name: string; + username: string; +}; + +export type UserList = ListMeta & { + items: User[]; +}; + +export type UserCreateRequest = { + email?: string; + name: string; + username: string; +}; + +export type UserPatchRequest = { + email?: string; + name?: string; + username?: string; +}; + +export class UserBuilder { + private data: Record = {}; + + + email(value: string): this { + this.data['email'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + username(value: string): this { + this.data['username'] = value; + return this; + } + + build(): UserCreateRequest { + if (!this.data['name']) { + throw new Error('name is required'); + } + if (!this.data['username']) { + throw new Error('username is required'); + } + return this.data as UserCreateRequest; + } +} + +export class UserPatchBuilder { + private data: Record = {}; + + + email(value: string): this { + this.data['email'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + username(value: string): this { + this.data['username'] = value; + return this; + } + + build(): UserPatchRequest { + return this.data as UserPatchRequest; + } +} + diff --git a/components/ambient-sdk/ts-sdk/src/user_api.ts b/components/ambient-sdk/ts-sdk/src/user_api.ts new file mode 100644 index 000000000..c76bcadfa --- /dev/null +++ b/components/ambient-sdk/ts-sdk/src/user_api.ts @@ -0,0 +1,42 @@ +// Code generated by ambient-sdk-generator from openapi.yaml — DO NOT EDIT. +// Source: ../../ambient-api-server/openapi/openapi.yaml +// Spec SHA256: 47119590c5b99c8ae2cf4280c6cd43829ffd283a58c334f22ff8aedc01851068 +// Generated: 2026-02-26T15:57:52Z + +import type { AmbientClientConfig, ListOptions, RequestOptions } from './base'; +import { ambientFetch, buildQueryString } from './base'; +import type { User, UserList, UserCreateRequest, UserPatchRequest } from './user'; + +export class UserAPI { + constructor(private readonly config: AmbientClientConfig) {} + + async create(data: UserCreateRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'POST', '/users', data, opts); + } + + async get(id: string, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'GET', `/users/${id}`, undefined, opts); + } + + async list(listOpts?: ListOptions, opts?: RequestOptions): Promise { + const qs = buildQueryString(listOpts); + return ambientFetch(this.config, 'GET', `/users${qs}`, undefined, opts); + } + async update(id: string, patch: UserPatchRequest, opts?: RequestOptions): Promise { + return ambientFetch(this.config, 'PATCH', `/users/${id}`, patch, opts); + } + + async *listAll(size: number = 100, opts?: RequestOptions): AsyncGenerator { + let page = 1; + while (true) { + const result = await this.list({ page, size }, opts); + for (const item of result.items) { + yield item; + } + if (page * size >= result.total) { + break; + } + page++; + } + } +} diff --git a/components/ambient-sdk/ts-sdk/tests/base.test.ts b/components/ambient-sdk/ts-sdk/tests/base.test.ts new file mode 100644 index 000000000..f58c141b1 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/tests/base.test.ts @@ -0,0 +1,70 @@ +import { buildQueryString, AmbientAPIError } from '../src'; +import type { ListOptions, APIError } from '../src'; + +describe('buildQueryString', () => { + it('returns empty string for undefined opts', () => { + expect(buildQueryString(undefined)).toBe(''); + }); + + it('returns empty string for empty opts', () => { + expect(buildQueryString({})).toBe(''); + }); + + it('builds page and size', () => { + const qs = buildQueryString({ page: 2, size: 50 }); + expect(qs).toContain('page=2'); + expect(qs).toContain('size=50'); + expect(qs.startsWith('?')).toBe(true); + }); + + it('caps size at 65500', () => { + const qs = buildQueryString({ size: 100000 }); + expect(qs).toContain('size=65500'); + }); + + it('includes search param', () => { + const qs = buildQueryString({ search: 'test query' }); + expect(qs).toContain('search='); + }); + + it('includes orderBy param', () => { + const qs = buildQueryString({ orderBy: 'created_at desc' }); + expect(qs).toContain('orderBy='); + }); + + it('includes fields param', () => { + const qs = buildQueryString({ fields: 'id,name,phase' }); + expect(qs).toContain('fields='); + }); + + it('combines multiple params', () => { + const qs = buildQueryString({ page: 1, size: 25, search: 'test' }); + expect(qs).toContain('page=1'); + expect(qs).toContain('size=25'); + expect(qs).toContain('search=test'); + }); +}); + +describe('AmbientAPIError', () => { + it('is instanceof Error', () => { + const err = new AmbientAPIError({ + id: '', kind: 'Error', href: '', + code: 'forbidden', reason: 'Access denied', + operation_id: '', status_code: 403, + }); + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(AmbientAPIError); + }); + + it('exposes structured fields', () => { + const err = new AmbientAPIError({ + id: 'err-1', kind: 'Error', href: '/errors/err-1', + code: 'validation_error', reason: 'Invalid field', + operation_id: 'op-123', status_code: 422, + }); + expect(err.statusCode).toBe(422); + expect(err.code).toBe('validation_error'); + expect(err.reason).toBe('Invalid field'); + expect(err.operationId).toBe('op-123'); + }); +}); diff --git a/components/ambient-sdk/ts-sdk/tests/client.test.ts b/components/ambient-sdk/ts-sdk/tests/client.test.ts new file mode 100644 index 000000000..77dd509a3 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/tests/client.test.ts @@ -0,0 +1,150 @@ +import { AmbientClient, AmbientAPIError } from '../src'; + +describe('AmbientClient construction', () => { + it('creates client with valid config', () => { + const client = new AmbientClient({ + baseUrl: 'https://api.ambient-platform.com', + token: 'sha256~abcdefghijklmnopqrstuvwxyz1234567890', + project: 'test-project', + }); + expect(client).toBeDefined(); + expect(client.sessions).toBeDefined(); + expect(client.projects).toBeDefined(); + expect(client.projectSettings).toBeDefined(); + expect(client.users).toBeDefined(); + }); + + it('throws when baseUrl is missing', () => { + expect(() => new AmbientClient({ + baseUrl: '', + token: 'sha256~abcdefghijklmnopqrstuvwxyz1234567890', + project: 'test-project', + })).toThrow('baseUrl is required'); + }); + + it('throws when token is missing', () => { + expect(() => new AmbientClient({ + baseUrl: 'https://api.ambient-platform.com', + token: '', + project: 'test-project', + })).toThrow('token is required'); + }); + + it('throws when project is missing', () => { + expect(() => new AmbientClient({ + baseUrl: 'https://api.ambient-platform.com', + token: 'sha256~abcdefghijklmnopqrstuvwxyz1234567890', + project: '', + })).toThrow('project is required'); + }); + + it('strips trailing slashes from baseUrl', () => { + const client = new AmbientClient({ + baseUrl: 'https://api.example.com///', + token: 'sha256~abcdefghijklmnopqrstuvwxyz1234567890', + project: 'test-project', + }); + expect(client).toBeDefined(); + }); +}); + +describe('AmbientClient.fromEnv', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('creates client from environment variables', () => { + process.env.AMBIENT_API_URL = 'https://api.test.com'; + process.env.AMBIENT_TOKEN = 'sha256~testtoken123'; + process.env.AMBIENT_PROJECT = 'my-project'; + const client = AmbientClient.fromEnv(); + expect(client).toBeDefined(); + expect(client.sessions).toBeDefined(); + }); + + it('throws when AMBIENT_TOKEN is missing', () => { + delete process.env.AMBIENT_TOKEN; + process.env.AMBIENT_PROJECT = 'my-project'; + expect(() => AmbientClient.fromEnv()).toThrow('AMBIENT_TOKEN environment variable is required'); + }); + + it('throws when AMBIENT_PROJECT is missing', () => { + process.env.AMBIENT_TOKEN = 'sha256~abcdefghijklmnopqrstuvwxyz1234567890'; + delete process.env.AMBIENT_PROJECT; + expect(() => AmbientClient.fromEnv()).toThrow('AMBIENT_PROJECT environment variable is required'); + }); +}); + +describe('AmbientAPIError', () => { + it('formats error message correctly', () => { + const error = new AmbientAPIError({ + id: '', + kind: 'Error', + href: '', + code: 'not_found', + reason: 'Session not found', + operation_id: '', + status_code: 404, + }); + expect(error.message).toBe('ambient API error 404: not_found — Session not found'); + expect(error.statusCode).toBe(404); + expect(error.code).toBe('not_found'); + expect(error.reason).toBe('Session not found'); + expect(error.name).toBe('AmbientAPIError'); + expect(error).toBeInstanceOf(Error); + }); +}); + +describe('Resource API accessor properties', () => { + const client = new AmbientClient({ + baseUrl: 'https://api.test.com', + token: 'sha256~abcdefghijklmnopqrstuvwxyz1234567890', + project: 'test-project', + }); + + const resourcesWithUpdate = [ + 'sessions', 'projectSettings', + ] as const; + + for (const name of resourcesWithUpdate) { + it(`${name} API has CRUD methods`, () => { + const api = client[name] as Record; + expect(typeof api.create).toBe('function'); + expect(typeof api.get).toBe('function'); + expect(typeof api.list).toBe('function'); + expect(typeof api.update).toBe('function'); + expect(typeof api.listAll).toBe('function'); + }); + } + + const resourcesWithDelete = [ + 'projects', 'projectSettings', + ] as const; + + for (const name of resourcesWithDelete) { + it(`${name} API has delete method`, () => { + const api = client[name] as Record; + expect(typeof api.delete).toBe('function'); + }); + } + + it('sessions API has start/stop/updateStatus methods', () => { + expect(typeof client.sessions.start).toBe('function'); + expect(typeof client.sessions.stop).toBe('function'); + expect(typeof client.sessions.updateStatus).toBe('function'); + }); + + it('users API has basic CRUD methods', () => { + const api = client.users as Record; + expect(typeof api.create).toBe('function'); + expect(typeof api.get).toBe('function'); + expect(typeof api.list).toBe('function'); + expect(typeof api.listAll).toBe('function'); + }); +}); diff --git a/components/ambient-sdk/ts-sdk/tests/types.test.ts b/components/ambient-sdk/ts-sdk/tests/types.test.ts new file mode 100644 index 000000000..83923b237 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/tests/types.test.ts @@ -0,0 +1,204 @@ +import { + SessionBuilder, + SessionPatchBuilder, + SessionStatusPatchBuilder, + ProjectBuilder, + ProjectPatchBuilder, + ProjectSettingsBuilder, + ProjectSettingsPatchBuilder, + UserBuilder, +} from '../src'; +import type { + Session, + SessionList, + SessionCreateRequest, + SessionPatchRequest, + SessionStatusPatchRequest, + Project, + ProjectList, + ProjectSettings, + ProjectSettingsList, + User, + UserList, + ObjectReference, + ListMeta, + ListOptions, +} from '../src'; + +describe('ObjectReference base type', () => { + it('should have correct shape', () => { + const ref: ObjectReference = { + id: 'abc123', + kind: 'Session', + href: '/sessions/abc123', + created_at: '2026-01-01T00:00:00Z', + updated_at: null, + }; + expect(ref.id).toBe('abc123'); + expect(ref.kind).toBe('Session'); + expect(ref.updated_at).toBeNull(); + }); +}); + +describe('ListMeta base type', () => { + it('should have correct shape', () => { + const meta: ListMeta = { kind: 'SessionList', page: 1, size: 100, total: 250 }; + expect(meta.page).toBe(1); + expect(meta.total).toBe(250); + }); +}); + +describe('ListOptions type', () => { + it('should accept partial options', () => { + const opts: ListOptions = { page: 2, size: 50 }; + expect(opts.page).toBe(2); + expect(opts.search).toBeUndefined(); + }); +}); + +describe('Session types', () => { + it('Session type extends ObjectReference', () => { + const session: Session = { + id: 'sess-1', + kind: 'Session', + href: '/sessions/sess-1', + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + name: 'test-session', + prompt: 'do something', + phase: 'pending', + timeout: 3600, + llm_model: 'claude-sonnet-4-20250514', + llm_temperature: 0.7, + llm_max_tokens: 4096, + annotations: '', + assigned_user_id: '', + bot_account_name: '', + completion_time: '', + conditions: '', + created_by_user_id: '', + environment_variables: '', + kube_cr_name: '', + kube_cr_uid: '', + kube_namespace: '', + labels: '', + parent_session_id: '', + project_id: '', + reconciled_repos: '', + reconciled_workflow: '', + repo_url: '', + repos: '', + resource_overrides: '', + sdk_restart_count: 0, + sdk_session_id: '', + start_time: '', + workflow_id: '', + }; + expect(session.id).toBe('sess-1'); + expect(session.name).toBe('test-session'); + expect(session.phase).toBe('pending'); + }); + + it('SessionList has items array', () => { + const list: SessionList = { + kind: 'SessionList', + page: 1, + size: 10, + total: 1, + items: [], + }; + expect(list.items).toHaveLength(0); + expect(list.total).toBe(1); + }); +}); + +describe('SessionBuilder', () => { + it('builds a valid create request with fluent API', () => { + const req = new SessionBuilder() + .name('my-session') + .prompt('analyze code') + .llmModel('claude-sonnet-4-20250514') + .timeout(3600) + .build(); + expect(req.name).toBe('my-session'); + expect(req.prompt).toBe('analyze code'); + expect(req.llm_model).toBe('claude-sonnet-4-20250514'); + expect(req.timeout).toBe(3600); + }); + + it('throws when name is missing', () => { + expect(() => new SessionBuilder().prompt('test').build()).toThrow('name is required'); + }); +}); + +describe('SessionPatchBuilder', () => { + it('builds a partial patch', () => { + const patch = new SessionPatchBuilder() + .prompt('updated prompt') + .timeout(7200) + .build(); + expect(patch.prompt).toBe('updated prompt'); + expect(patch.timeout).toBe(7200); + expect(patch.name).toBeUndefined(); + }); +}); + +describe('SessionStatusPatchBuilder', () => { + it('builds a status patch', () => { + const patch = new SessionStatusPatchBuilder() + .phase('running') + .startTime('2026-01-01T00:00:00Z') + .build(); + expect(patch.phase).toBe('running'); + expect(patch.start_time).toBe('2026-01-01T00:00:00Z'); + expect(patch.completion_time).toBeUndefined(); + }); +}); + +describe('All 4 resource builders exist and build', () => { + it('SessionBuilder', () => { + const req = new SessionBuilder().name('session-1').build(); + expect(req.name).toBe('session-1'); + }); + + it('ProjectBuilder', () => { + const req = new ProjectBuilder().name('project-1').build(); + expect(req.name).toBe('project-1'); + }); + + it('ProjectSettingsBuilder', () => { + const req = new ProjectSettingsBuilder().projectId('proj-1').build(); + expect(req.project_id).toBe('proj-1'); + }); + + it('UserBuilder', () => { + const req = new UserBuilder().name('user-1').username('user1').build(); + expect(req.name).toBe('user-1'); + expect(req.username).toBe('user1'); + }); + + it('UserBuilder throws when name is missing', () => { + expect(() => new UserBuilder().username('user1').build()).toThrow('name is required'); + }); + + it('UserBuilder throws when username is missing', () => { + expect(() => new UserBuilder().name('user-1').build()).toThrow('username is required'); + }); +}); + +describe('PatchBuilder for each resource', () => { + it('SessionPatchBuilder exists', () => { + const patch = new SessionPatchBuilder().name('updated').build(); + expect(patch.name).toBe('updated'); + }); + + it('ProjectPatchBuilder exists', () => { + const patch = new ProjectPatchBuilder().name('updated').build(); + expect(patch.name).toBe('updated'); + }); + + it('ProjectSettingsPatchBuilder exists', () => { + const patch = new ProjectSettingsPatchBuilder().projectId('proj-2').build(); + expect(patch.project_id).toBe('proj-2'); + }); +}); \ No newline at end of file diff --git a/components/ambient-sdk/ts-sdk/tsconfig.json b/components/ambient-sdk/ts-sdk/tsconfig.json new file mode 100644 index 000000000..855aa243f --- /dev/null +++ b/components/ambient-sdk/ts-sdk/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/components/ambient-sdk/ts-sdk/tsconfig.test.json b/components/ambient-sdk/ts-sdk/tsconfig.test.json new file mode 100644 index 000000000..e74ef58a8 --- /dev/null +++ b/components/ambient-sdk/ts-sdk/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/components/manifests/overlays/production/kustomization.yaml b/components/manifests/overlays/production/kustomization.yaml index a21137659..2ba81541a 100644 --- a/components/manifests/overlays/production/kustomization.yaml +++ b/components/manifests/overlays/production/kustomization.yaml @@ -35,7 +35,7 @@ patches: kind: Service name: frontend-service - path: unleash-init-db-patch.yaml -- path: postgresql-json-patch.yaml +#- path: postgresql-json-patch.yaml target: group: apps kind: Deployment