nebelus 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ # Publishes the `nebelus` package to PyPI via Trusted Publishing (OIDC) —
2
+ # no API token exists anywhere. Requires the PyPI-side "pending publisher"
3
+ # to be configured for: owner Nebelus, repo nebelus-python, workflow
4
+ # release.yml, environment pypi.
5
+ name: release
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: pip install -e ".[dev]"
18
+ - run: ruff check src tests
19
+ - run: pytest tests -q
20
+ publish:
21
+ needs: test
22
+ runs-on: ubuntu-latest
23
+ environment: pypi
24
+ permissions:
25
+ id-token: write # OIDC for PyPI Trusted Publishing
26
+ contents: read
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+ - run: pip install build
33
+ - run: python -m build
34
+ - uses: pypa/gh-action-pypi-publish@release/v1
nebelus-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.5
2
+ Name: nebelus
3
+ Version: 0.1.1
4
+ Summary: Nebelus Agents API — build, edit, and ship governed AI agents from code
5
+ Author-email: Nebelus <support@nebelus.ai>
6
+ License: Proprietary
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: pydantic>=2.5
10
+ Provides-Extra: dev
11
+ Requires-Dist: langgraph>=0.2; extra == 'dev'
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Requires-Dist: respx>=0.21; extra == 'dev'
14
+ Requires-Dist: ruff>=0.6; extra == 'dev'
15
+ Provides-Extra: langgraph
16
+ Requires-Dist: langgraph>=0.2; extra == 'langgraph'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Nebelus Agents API — Python SDK
20
+
21
+ Build, edit, and ship governed AI agents from code. Every agent you create here is the
22
+ same artifact your team sees in the Nebelus portal — one construction service, every
23
+ surface, your organization's governance applied identically.
24
+
25
+ ```python
26
+ from nebelus import Nebelus, AgentManifest
27
+
28
+ nb = Nebelus() # NEBELUS_API_KEY + NEBELUS_BASE_URL (default https://api.nebelus.ai)
29
+
30
+ # Discover what this organization can build — machine-readable.
31
+ info = nb.describe()
32
+
33
+ manifest = AgentManifest(
34
+ name="Return-policy concierge",
35
+ model_id="claude-haiku-4-5",
36
+ system_message="Answer from the return policy. Escalate anything ambiguous.",
37
+ )
38
+ agent = nb.apply(manifest) # create-or-update, key-wise merge — never clobbers portal edits
39
+ print(nb.agents.probe(agent.id, "Can I return a jacket after 20 days?").reply)
40
+ # Deploying is a human act unless your org opted in to programmatic deployment:
41
+ # nb.agents.deploy(agent.id) # needs the api.construction.deploy scope + the org opt-in
42
+ ```
43
+
44
+ Keys: Nebelus portal → Settings → API keys (administrator-managed). Scopes:
45
+ `api.construction.read`, `api.construction.write`, and `api.construction.deploy`.
46
+ Your Build Envelope (if your organization uses one) applies to code exactly as it applies
47
+ to every other surface.
48
+
49
+ ## CLI
50
+
51
+ Everything above is also a command (`pip install nebelus` puts `nebelus` on your PATH):
52
+
53
+ ```bash
54
+ nebelus describe # everything your org can build, machine-readable
55
+ nebelus catalog --view tools --query crm
56
+ nebelus apply agent.py # a file defining `manifest = AgentManifest(...)`
57
+ nebelus diff agent.py # what apply would change ("in sync" when nothing)
58
+ nebelus validate <agent-id> # pre-flight findings before you probe or deploy
59
+ nebelus probe <agent-id> "Hi there" # run the draft through the real runtime
60
+ nebelus export <agent-id> > agent.py # a live agent as a maintainable Python manifest
61
+ nebelus deploy <agent-id> # needs the deploy scope + the org's opt-in
62
+ ```
63
+
64
+ ## Two-way with the portal
65
+
66
+ `nebelus export` (or `nebelus.export_to_code`) turns any live agent — including one a
67
+ colleague built visually — into a Python manifest you own in git. `apply` takes it back.
68
+ The merge contract makes this safe in both directions: a manifest only manages the fields
69
+ it declares, so portal edits to everything else survive every apply, and `diff` never
70
+ reports server-side normalization as drift.
71
+
72
+ ## Coming from LangGraph
73
+
74
+ `from_langgraph` translates a `StateGraph`'s topology into a Nebelus workflow — nodes,
75
+ edges, and conditional-edge targets map mechanically (both sides share the
76
+ `__start__`/`__end__` sentinels). What a node *does* and how a router *decides* live in
77
+ your Python, so you declare those explicitly; anything with no declarative equivalent
78
+ stays in your code and attaches to the agent as an MCP server or custom API endpoint.
79
+ Incomplete translations return named diagnostics instead of a manifest — nothing is
80
+ guessed:
81
+
82
+ ```python
83
+ from nebelus import Nebelus, from_langgraph
84
+
85
+ t = from_langgraph(
86
+ my_state_graph,
87
+ node_map={"triage": {"type": "agent", "config": {"system_prompt": "...", "model_id": "claude-haiku-4-5"}}},
88
+ router_map={"triage": {"conditions": [{"expression": "...", "target": "billing"}],
89
+ "default_target": "general"}},
90
+ manifest_id="triage-v1", name="Triage", model_id="claude-haiku-4-5",
91
+ )
92
+ if t.complete:
93
+ Nebelus().apply(t.manifest)
94
+ else:
95
+ print("\n".join(t.diagnostics)) # names every unmapped node and undeclared router
96
+ ```
97
+
98
+ Install the source-graph dependency with `pip install "nebelus[langgraph]"`.
99
+
100
+ ## GitHub Action
101
+
102
+ Keep manifests in git and let CI hold them in sync — PRs show the diff, merges apply it
103
+ (see `examples/agents-apply.yml`):
104
+
105
+ ```yaml
106
+ - uses: Nebelus/nebelus-python/action@main
107
+ with:
108
+ manifests: agents/*.py
109
+ api-key: ${{ secrets.NEBELUS_API_KEY }}
110
+ mode: ${{ github.event_name == 'push' && 'apply' || 'diff' }}
111
+ ```
112
+
113
+ ## Rate limits
114
+
115
+ The API rate-limits per key (generous defaults; probes are tighter since each spends real
116
+ model money). The SDK waits out short `Retry-After` pauses automatically and raises
117
+ `nebelus.RateLimited` (with `.retry_after`) when the pause is too long to hold in-process.
@@ -0,0 +1,99 @@
1
+ # Nebelus Agents API — Python SDK
2
+
3
+ Build, edit, and ship governed AI agents from code. Every agent you create here is the
4
+ same artifact your team sees in the Nebelus portal — one construction service, every
5
+ surface, your organization's governance applied identically.
6
+
7
+ ```python
8
+ from nebelus import Nebelus, AgentManifest
9
+
10
+ nb = Nebelus() # NEBELUS_API_KEY + NEBELUS_BASE_URL (default https://api.nebelus.ai)
11
+
12
+ # Discover what this organization can build — machine-readable.
13
+ info = nb.describe()
14
+
15
+ manifest = AgentManifest(
16
+ name="Return-policy concierge",
17
+ model_id="claude-haiku-4-5",
18
+ system_message="Answer from the return policy. Escalate anything ambiguous.",
19
+ )
20
+ agent = nb.apply(manifest) # create-or-update, key-wise merge — never clobbers portal edits
21
+ print(nb.agents.probe(agent.id, "Can I return a jacket after 20 days?").reply)
22
+ # Deploying is a human act unless your org opted in to programmatic deployment:
23
+ # nb.agents.deploy(agent.id) # needs the api.construction.deploy scope + the org opt-in
24
+ ```
25
+
26
+ Keys: Nebelus portal → Settings → API keys (administrator-managed). Scopes:
27
+ `api.construction.read`, `api.construction.write`, and `api.construction.deploy`.
28
+ Your Build Envelope (if your organization uses one) applies to code exactly as it applies
29
+ to every other surface.
30
+
31
+ ## CLI
32
+
33
+ Everything above is also a command (`pip install nebelus` puts `nebelus` on your PATH):
34
+
35
+ ```bash
36
+ nebelus describe # everything your org can build, machine-readable
37
+ nebelus catalog --view tools --query crm
38
+ nebelus apply agent.py # a file defining `manifest = AgentManifest(...)`
39
+ nebelus diff agent.py # what apply would change ("in sync" when nothing)
40
+ nebelus validate <agent-id> # pre-flight findings before you probe or deploy
41
+ nebelus probe <agent-id> "Hi there" # run the draft through the real runtime
42
+ nebelus export <agent-id> > agent.py # a live agent as a maintainable Python manifest
43
+ nebelus deploy <agent-id> # needs the deploy scope + the org's opt-in
44
+ ```
45
+
46
+ ## Two-way with the portal
47
+
48
+ `nebelus export` (or `nebelus.export_to_code`) turns any live agent — including one a
49
+ colleague built visually — into a Python manifest you own in git. `apply` takes it back.
50
+ The merge contract makes this safe in both directions: a manifest only manages the fields
51
+ it declares, so portal edits to everything else survive every apply, and `diff` never
52
+ reports server-side normalization as drift.
53
+
54
+ ## Coming from LangGraph
55
+
56
+ `from_langgraph` translates a `StateGraph`'s topology into a Nebelus workflow — nodes,
57
+ edges, and conditional-edge targets map mechanically (both sides share the
58
+ `__start__`/`__end__` sentinels). What a node *does* and how a router *decides* live in
59
+ your Python, so you declare those explicitly; anything with no declarative equivalent
60
+ stays in your code and attaches to the agent as an MCP server or custom API endpoint.
61
+ Incomplete translations return named diagnostics instead of a manifest — nothing is
62
+ guessed:
63
+
64
+ ```python
65
+ from nebelus import Nebelus, from_langgraph
66
+
67
+ t = from_langgraph(
68
+ my_state_graph,
69
+ node_map={"triage": {"type": "agent", "config": {"system_prompt": "...", "model_id": "claude-haiku-4-5"}}},
70
+ router_map={"triage": {"conditions": [{"expression": "...", "target": "billing"}],
71
+ "default_target": "general"}},
72
+ manifest_id="triage-v1", name="Triage", model_id="claude-haiku-4-5",
73
+ )
74
+ if t.complete:
75
+ Nebelus().apply(t.manifest)
76
+ else:
77
+ print("\n".join(t.diagnostics)) # names every unmapped node and undeclared router
78
+ ```
79
+
80
+ Install the source-graph dependency with `pip install "nebelus[langgraph]"`.
81
+
82
+ ## GitHub Action
83
+
84
+ Keep manifests in git and let CI hold them in sync — PRs show the diff, merges apply it
85
+ (see `examples/agents-apply.yml`):
86
+
87
+ ```yaml
88
+ - uses: Nebelus/nebelus-python/action@main
89
+ with:
90
+ manifests: agents/*.py
91
+ api-key: ${{ secrets.NEBELUS_API_KEY }}
92
+ mode: ${{ github.event_name == 'push' && 'apply' || 'diff' }}
93
+ ```
94
+
95
+ ## Rate limits
96
+
97
+ The API rate-limits per key (generous defaults; probes are tighter since each spends real
98
+ model money). The SDK waits out short `Retry-After` pauses automatically and raises
99
+ `nebelus.RateLimited` (with `.retry_after`) when the pause is too long to hold in-process.
@@ -0,0 +1,39 @@
1
+ name: "Nebelus Agents Apply"
2
+ description: "Diff or apply Nebelus agent manifests (.py defining `manifest = AgentManifest(...)`) from your repo."
3
+ branding:
4
+ icon: upload-cloud
5
+ color: orange
6
+ inputs:
7
+ manifests:
8
+ description: "Whitespace-separated manifest file paths"
9
+ required: true
10
+ api-key:
11
+ description: "Nebelus API key (store as a repo secret)"
12
+ required: true
13
+ base-url:
14
+ description: "API base URL"
15
+ required: false
16
+ default: "https://api.nebelus.ai"
17
+ mode:
18
+ description: "'diff' (report only — use on pull_request) or 'apply' (use on push to main)"
19
+ required: false
20
+ default: "diff"
21
+ runs:
22
+ using: composite
23
+ steps:
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+ - shell: bash
28
+ run: pip install --quiet nebelus
29
+ - shell: bash
30
+ env:
31
+ NEBELUS_API_KEY: ${{ inputs.api-key }}
32
+ NEBELUS_BASE_URL: ${{ inputs.base-url }}
33
+ run: |
34
+ set -euo pipefail
35
+ for f in ${{ inputs.manifests }}; do
36
+ echo "::group::${{ inputs.mode }} $f"
37
+ nebelus "${{ inputs.mode }}" "$f"
38
+ echo "::endgroup::"
39
+ done
@@ -0,0 +1,20 @@
1
+ # Copy to .github/workflows/ in the repo that holds your agent manifests.
2
+ # PRs show the diff; merges to main apply it. Deploys stay a human act unless
3
+ # your org opted in and the key carries the deploy scope.
4
+ name: Agents
5
+ on:
6
+ pull_request:
7
+ paths: ["agents/**.py"]
8
+ push:
9
+ branches: [main]
10
+ paths: ["agents/**.py"]
11
+ jobs:
12
+ agents:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: Nebelus/nebelus-python/action@main
17
+ with:
18
+ manifests: agents/*.py
19
+ api-key: ${{ secrets.NEBELUS_API_KEY }}
20
+ mode: ${{ github.event_name == 'push' && 'apply' || 'diff' }}
@@ -0,0 +1,34 @@
1
+ """A complete workflow agent as code — apply with `nebelus apply examples/triage_workflow.py`."""
2
+
3
+ from nebelus import AgentManifest
4
+
5
+ manifest = AgentManifest(
6
+ manifest_id="triage-example-v1",
7
+ name="Support triage",
8
+ model_id="claude-haiku-4-5",
9
+ pattern_type="workflow",
10
+ description="Routes billing questions to a specialist, everything else to a generalist.",
11
+ pattern_config={
12
+ "nodes": [
13
+ {"name": "triage", "type": "agent",
14
+ "config": {"system_prompt": "Classify the request as 'billing' or 'general'. Reply with exactly one word.",
15
+ "model_id": "claude-haiku-4-5"}},
16
+ {"name": "router", "type": "condition",
17
+ "config": {"conditions": [{"expression": "'billing' in str(state.get('triage_out','')).lower()",
18
+ "target": "billing"}],
19
+ "default_target": "general"}},
20
+ {"name": "billing", "type": "agent",
21
+ "config": {"system_prompt": "You are the billing specialist. Be precise.",
22
+ "model_id": "claude-haiku-4-5"}},
23
+ {"name": "general", "type": "agent",
24
+ "config": {"system_prompt": "You are the general assistant. Be brief.",
25
+ "model_id": "claude-haiku-4-5"}},
26
+ ],
27
+ "edges": [
28
+ {"from": "__start__", "to": "triage"},
29
+ {"from": "triage", "to": "router"},
30
+ {"from": "billing", "to": "__end__"},
31
+ {"from": "general", "to": "__end__"},
32
+ ],
33
+ },
34
+ )
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "nebelus"
7
+ version = "0.1.1"
8
+ description = "Nebelus Agents API — build, edit, and ship governed AI agents from code"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Proprietary" }
12
+ authors = [{ name = "Nebelus", email = "support@nebelus.ai" }]
13
+ dependencies = ["httpx>=0.27", "pydantic>=2.5"]
14
+
15
+ [project.scripts]
16
+ nebelus = "nebelus.cli:main"
17
+
18
+ [project.optional-dependencies]
19
+ dev = ["pytest>=8", "respx>=0.21", "ruff>=0.6", "langgraph>=0.2"]
20
+ langgraph = ["langgraph>=0.2"]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/nebelus"]
24
+
25
+ [tool.ruff]
26
+ line-length = 120
@@ -0,0 +1,22 @@
1
+ """Nebelus Agents API — official Python SDK."""
2
+
3
+ from ._transport import NebelusAPIError, NotFound, RateLimited
4
+ from .client import Nebelus
5
+ from .export import export_to_code
6
+ from .langgraph import Translation, from_langgraph
7
+ from .models import Agent, AgentManifest, ProbeResult, ValidationResult
8
+
9
+ __version__ = "0.1.1"
10
+ __all__ = [
11
+ "Agent",
12
+ "AgentManifest",
13
+ "Nebelus",
14
+ "NebelusAPIError",
15
+ "NotFound",
16
+ "ProbeResult",
17
+ "RateLimited",
18
+ "Translation",
19
+ "ValidationResult",
20
+ "export_to_code",
21
+ "from_langgraph",
22
+ ]
@@ -0,0 +1,103 @@
1
+ """HTTP transport: auth, errors-as-exceptions with the API's machine payload intact."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ DEFAULT_BASE_URL = "https://api.nebelus.ai"
12
+ API_PREFIX = "/api/v1/construction"
13
+
14
+ # 429s are retried automatically when the server's Retry-After is short enough
15
+ # to wait out in-process; longer waits surface as RateLimited for the caller.
16
+ MAX_RETRY_AFTER_SECONDS = 30.0
17
+ MAX_RATE_LIMIT_RETRIES = 2
18
+
19
+
20
+ class NebelusAPIError(Exception):
21
+ """Any non-2xx from the API. Carries the FULL machine-readable payload:
22
+ `detail` (human reason), and when present `envelope` ({blocked, requested,
23
+ allowed_hint}), `blocked` (opt-in gates), etc."""
24
+
25
+ def __init__(self, status_code: int, payload: Any):
26
+ self.status_code = status_code
27
+ self.payload = payload if isinstance(payload, dict) else {"detail": str(payload)}
28
+ super().__init__(f"[{status_code}] {self.payload.get('detail') or self.payload}")
29
+
30
+ @property
31
+ def detail(self) -> str:
32
+ return str(self.payload.get("detail", ""))
33
+
34
+ @property
35
+ def envelope(self) -> dict | None:
36
+ """The Build Envelope refusal payload, when the refusal came from one."""
37
+ return self.payload.get("envelope")
38
+
39
+ @property
40
+ def blocked(self) -> str | None:
41
+ return self.payload.get("blocked")
42
+
43
+
44
+ class NotFound(NebelusAPIError):
45
+ pass
46
+
47
+
48
+ class RateLimited(NebelusAPIError):
49
+ """429 that could not be waited out in-process. `retry_after` is the
50
+ server's requested pause in seconds, when it sent one."""
51
+
52
+ def __init__(self, status_code: int, payload: Any, retry_after: float | None = None):
53
+ super().__init__(status_code, payload)
54
+ self.retry_after = retry_after
55
+
56
+
57
+ class Transport:
58
+ def __init__(self, api_key: str | None = None, base_url: str | None = None, timeout: float = 60.0):
59
+ self.api_key = api_key or os.environ.get("NEBELUS_API_KEY") or ""
60
+ if not self.api_key:
61
+ raise ValueError("No API key. Pass api_key= or set NEBELUS_API_KEY.")
62
+ self.base_url = (base_url or os.environ.get("NEBELUS_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
63
+ self._client = httpx.Client(
64
+ base_url=f"{self.base_url}{API_PREFIX}",
65
+ headers={"Authorization": f"Bearer {self.api_key}", "User-Agent": "nebelus-python/0.1.1"},
66
+ timeout=timeout,
67
+ )
68
+
69
+ def request(self, method: str, path: str, *, json: Any = None, params: Any = None) -> Any:
70
+ for attempt in range(MAX_RATE_LIMIT_RETRIES + 1):
71
+ r = self._client.request(method, path, json=json, params=params)
72
+ if r.status_code != 429:
73
+ break
74
+ retry_after = _retry_after_seconds(r)
75
+ if attempt == MAX_RATE_LIMIT_RETRIES or retry_after is None or retry_after > MAX_RETRY_AFTER_SECONDS:
76
+ raise RateLimited(429, _body_of(r), retry_after=retry_after)
77
+ time.sleep(retry_after)
78
+ body = _body_of(r)
79
+ if r.status_code == 404:
80
+ raise NotFound(r.status_code, body)
81
+ if r.status_code >= 400:
82
+ raise NebelusAPIError(r.status_code, body)
83
+ return body
84
+
85
+ def close(self) -> None:
86
+ self._client.close()
87
+
88
+
89
+ def _body_of(r: httpx.Response) -> Any:
90
+ try:
91
+ return r.json()
92
+ except Exception: # noqa: BLE001
93
+ return r.text
94
+
95
+
96
+ def _retry_after_seconds(r: httpx.Response) -> float | None:
97
+ raw = r.headers.get("Retry-After")
98
+ if raw is None:
99
+ return None
100
+ try:
101
+ return max(0.0, float(raw))
102
+ except ValueError:
103
+ return None
@@ -0,0 +1,85 @@
1
+ """`nebelus` — the CLI over the SDK. Same auth (NEBELUS_API_KEY / NEBELUS_BASE_URL)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.util
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from .client import Nebelus, NebelusAPIError
12
+ from .export import export_to_code
13
+ from .models import AgentManifest
14
+
15
+
16
+ def _load_manifest(path: str) -> AgentManifest:
17
+ p = Path(path)
18
+ if p.suffix == ".json":
19
+ return AgentManifest.model_validate(json.loads(p.read_text()))
20
+ spec = importlib.util.spec_from_file_location("nebelus_manifest", p)
21
+ mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
22
+ spec.loader.exec_module(mod) # type: ignore[union-attr]
23
+ manifest = getattr(mod, "manifest", None)
24
+ if not isinstance(manifest, AgentManifest):
25
+ raise SystemExit(f"{path} must define `manifest = AgentManifest(...)`")
26
+ return manifest
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> int:
30
+ ap = argparse.ArgumentParser(prog="nebelus", description="Nebelus Agents API CLI")
31
+ sub = ap.add_subparsers(dest="cmd", required=True)
32
+ sub.add_parser("describe", help="everything buildable in this org, machine-readable")
33
+ c = sub.add_parser("catalog", help="org catalog")
34
+ c.add_argument("--view", default="models")
35
+ c.add_argument("--query")
36
+ a = sub.add_parser("apply", help="create-or-update from a manifest (.py or .json)")
37
+ a.add_argument("path")
38
+ d = sub.add_parser("diff", help="what apply would change")
39
+ d.add_argument("path")
40
+ p = sub.add_parser("probe", help="run the draft for real")
41
+ p.add_argument("agent_id")
42
+ p.add_argument("message")
43
+ dep = sub.add_parser("deploy", help="deploy (needs org opt-in + deploy scope)")
44
+ dep.add_argument("agent_id")
45
+ e = sub.add_parser("export", help="export a live agent as a Python manifest")
46
+ e.add_argument("agent_id")
47
+ v = sub.add_parser("validate", help="pre-flight findings")
48
+ v.add_argument("agent_id")
49
+ args = ap.parse_args(argv)
50
+
51
+ nb = Nebelus()
52
+ try:
53
+ if args.cmd == "describe":
54
+ print(json.dumps(nb.describe(), indent=2, default=str))
55
+ elif args.cmd == "catalog":
56
+ print(json.dumps(nb.catalog(view=args.view, query=args.query), indent=2, default=str))
57
+ elif args.cmd == "apply":
58
+ agent = nb.apply(_load_manifest(args.path))
59
+ print(f"{agent.id} {agent.status}")
60
+ elif args.cmd == "diff":
61
+ changes = nb.diff(_load_manifest(args.path))
62
+ print(json.dumps(changes, indent=2, default=str) if changes else "in sync")
63
+ elif args.cmd == "probe":
64
+ r = nb.agents.probe(args.agent_id, args.message)
65
+ print(r.reply or r.model_dump())
66
+ elif args.cmd == "deploy":
67
+ print(json.dumps(nb.agents.deploy(args.agent_id)))
68
+ elif args.cmd == "validate":
69
+ print(json.dumps(nb.agents.validate(args.agent_id).model_dump(), indent=2))
70
+ elif args.cmd == "export":
71
+ print(export_to_code(nb.agents.get(args.agent_id)))
72
+ except NebelusAPIError as exc:
73
+ print(f"error [{exc.status_code}]: {exc.detail}", file=sys.stderr)
74
+ if exc.envelope:
75
+ print(f"envelope: {json.dumps(exc.envelope)}", file=sys.stderr)
76
+ if exc.blocked:
77
+ print(f"blocked: {exc.blocked}", file=sys.stderr)
78
+ return 1
79
+ finally:
80
+ nb.close()
81
+ return 0
82
+
83
+
84
+ if __name__ == "__main__":
85
+ raise SystemExit(main())